<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="generator" content="VNote">

    <title>5_对象关系映射（ORM）</title>
    <link rel="icon" href="https://github.com/tamlok/vnote/raw/master/src/resources/icons/vnote.ico">

    <style type="text/css">
    /* STYLE_GLOBAL_PLACE_HOLDER */
    </style>

    <style type="text/css">
    *,
*::before,
*::after {
  box-sizing: border-box;
}

.container-fluid {
    width: 100%;
    padding-right: 15px;
    padding-left: 15px;
    margin-right: auto;
    margin-left: auto;
}

.col, .col-1, .col-10, .col-11, .col-12, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-auto, .col-lg, .col-lg-1, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-auto, .col-md, .col-md-1, .col-md-10, .col-md-11, .col-md-12, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-auto, .col-sm, .col-sm-1, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-auto, .col-xl, .col-xl-1, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-auto {
    position: relative;
    width: 100%;
    min-height: 1px;
    padding-right: 15px;
    padding-left: 15px;
}

.col-12 {
    -webkit-box-flex: 0;
    -ms-flex: 0 0 100%;
    flex: 0 0 100%;
    max-width: 100%;
}

@media (min-width: 768px) {
    .col-md-3 {
        -webkit-box-flex: 0;
        -ms-flex: 0 0 25%;
        flex: 0 0 25%;
        max-width: 25%;
    }
}

@media (min-width: 768px) {
    .col-md-9 {
        -webkit-box-flex: 0;
        -ms-flex: 0 0 75%;
        flex: 0 0 75%;
        max-width: 75%;
    }
}

@media (min-width: 1200px) {
    .col-xl-2 {
        -webkit-box-flex: 0;
        -ms-flex: 0 0 16.666667%;
        flex: 0 0 16.666667%;
        max-width: 16.666667%;
    }
}

@media (min-width: 1200px) {
    .col-xl-10 {
        -webkit-box-flex: 0;
        -ms-flex: 0 0 83.333333%;
        flex: 0 0 83.333333%;
        max-width: 83.333333%;
    }
}

@media (min-width: 768px) {
    .pt-md-3, .py-md-3 {
        padding-top: 1rem!important;
    }
}

@media (min-width: 768px) {
    .pb-md-3, .py-md-3 {
        padding-bottom: 1rem!important;
    }
}

@media (min-width: 768px) {
    .pl-md-5, .px-md-5 {
        padding-left: 3rem!important;
    }
}

.d-none {
    display: none!important;
}

@media (min-width: 1200px) {
    .d-xl-block {
        display: block!important;
    }
}

@media (min-width: 768px) {
    .d-md-block {
        display: block!important;
    }
}

.bd-content {
    -webkit-box-ordinal-group: 1;
    -ms-flex-order: 0;
    order: 0;
}

.bd-toc {
    position: -webkit-sticky;
    position: sticky;
    top: 4rem;
    height: calc(100vh - 10rem);
    overflow-y: auto;
}

.bd-toc {
    -webkit-box-ordinal-group: 2;
    -ms-flex-order: 1;
    order: 1;
    padding-top: 1.5rem;
    padding-bottom: 1.5rem;
    font-size: .875rem;
}

.section-nav {
    padding-left: 0;
}

.section-nav ul {
    font-size: .875rem;
    list-style-type: none;
}

.section-nav li {
    font-size: .875rem;
}

.section-nav a {
    color: inherit !important;
}

.row {
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    -ms-flex-wrap: wrap;
    flex-wrap: wrap;
    margin-right: -15px;
    margin-left: -15px;
}

@media (min-width: 1200px) {
    .flex-xl-nowrap {
        flex-wrap: nowrap !important;
    }
}

#floating-button {
    width: 2.5rem;
    height: 2.5rem;
    border-radius: 50%;
    background: #00897B;
    position: fixed;
    top: .5rem;
    right: .5rem;
    cursor: pointer;
    box-shadow: 0px 2px 5px #666;
}

#floating-button .more {
    color: #F5F5F5;
    position: absolute;
    top: 0;
    display: block;
    bottom: 0;
    left: 0;
    right: 0;
    text-align: center;
    padding: 0;
    margin: 0;
    line-height: 2.5rem;
    font-size: 2rem;
    font-family: 'monospace';
    font-weight: 300;
}

.hide-none {
    display: none !important;
}

.col-expand {
    -webkit-box-flex: 0;
    -ms-flex: 0 0 100% !important;
    flex: 0 0 100% !important;
    max-width: 100% !important;
    padding-right: 3rem !important;
}

.outline-bold {
    font-weight: bolder !important;
}

@media print {
    #floating-button {
        display: none !important;
    }
}

    @keyframes flash { 
  0% { color: rgb(128, 203, 196); }
  10% { color: rgb(0, 137, 123); }
  40% { color: rgb(0, 137, 123); }
  50% { color: rgb(128, 203, 196); }
  60% { color: rgb(0, 137, 123); }
  90% { color: rgb(0, 137, 123); }
}
.highlighted-anchor { animation: flash 1s; }
div.mark-rect { background: transparent; border: 5px solid rgb(87, 104, 196); border-radius: 2px; position: absolute; }
#vnote-footer { width: 100%; text-align: center; opacity: 0.2; margin-top: 3rem; }
#vnote-footer p { font-size: 0.8rem; }
#vnote-footer a { color: inherit !important; }
x-eqs { display: flex; flex-direction: row; align-content: space-between; align-items: center; }
x-eqs > x-eqn { width: 100%; margin-left: 3rem; }
x-eqs > span { text-align: right; }
.view-image, .view-svg { transition: 0.3s; }
.modal-box { display: none; position: fixed; z-index: 1000; padding-top: 50px; left: 0px; top: 0px; width: 100%; height: 100%; overflow: hidden; background-color: rgba(68, 68, 68, 0.952941); }
.modal-content { margin: auto; display: block; width: auto; height: auto; cursor: move; }
.modal-content { animation-name: zoom; animation-duration: 0.6s; }
@-webkit-keyframes zoom { 
  0% { transform: scale(0); }
  100% { transform: scale(1); }
}
@keyframes zoom { 
  0% { transform: scale(0); }
  100% { transform: scale(1); }
}
span.modal-close { position: absolute; z-index: 1000; top: 15px; right: 35px; color: rgb(218, 218, 218); font-size: 40px; font-weight: bold; transition: 0.3s; }
span.modal-close:hover, span.modal-close:focus { color: rgb(238, 238, 238); text-decoration: none; cursor: pointer; }
@media print {
  pre, pre code, td.hljs-ln-code { white-space: pre-wrap !important; word-break: break-all !important; }
  code, a { word-break: break-all !important; }
  div.flowchart-diagram, div.mermaid-diagram, div.plantuml-diagram { overflow: hidden !important; }
  img { max-width: 100% !important; height: auto !important; }
  #vnote-footer { display: none !important; }
}
.alert { position: relative; padding: 0.75rem 1.25rem; margin-bottom: 1rem; border: 1px solid transparent; border-radius: 0.25rem; }
.alert-primary { color: rgb(0, 64, 133); background-color: rgb(204, 229, 255); border-color: rgb(184, 218, 255); }
.alert-secondary { color: rgb(56, 61, 65); background-color: rgb(226, 227, 229); border-color: rgb(214, 216, 219); }
.alert-success { color: rgb(21, 87, 36); background-color: rgb(212, 237, 218); border-color: rgb(195, 230, 203); }
.alert-info { color: rgb(12, 84, 96); background-color: rgb(209, 236, 241); border-color: rgb(190, 229, 235); }
.alert-warning { color: rgb(133, 100, 4); background-color: rgb(255, 243, 205); border-color: rgb(255, 238, 186); }
.alert-danger { color: rgb(114, 28, 36); background-color: rgb(248, 215, 218); border-color: rgb(245, 198, 203); }
.alert-light { color: rgb(129, 129, 130); background-color: rgb(254, 254, 254); border-color: rgb(253, 253, 254); }
.alert-dark { color: rgb(27, 30, 33); background-color: rgb(214, 216, 217); border-color: rgb(198, 200, 202); }
.vnote-anchor { font-weight: 400; color: rgba(0, 123, 255, 0.498039); transition: color 0.16s linear; padding-left: 0.375em; -webkit-font-smoothing: antialiased; text-decoration: none; opacity: 0; }
.vnote-anchor:hover { color: rgb(0, 123, 255); text-decoration: none; opacity: 1; }
.vnote-anchor::after { content: attr(data-anchor-icon); }
.vnote-btn { position: relative; display: inline-block; padding: 6px 12px; font-size: 13px; font-weight: 700; line-height: 20px; white-space: nowrap; vertical-align: middle; cursor: pointer; border: none; user-select: none; -webkit-appearance: none; }
.vnote-copy-clipboard-btn { transition: opacity 0.3s ease-in-out; opacity: 0; padding: 2px 6px; position: absolute; top: 5px; right: 5px; }
pre:hover .vnote-copy-clipboard-btn { opacity: 1; }
pre.vnote-snippet { position: relative; }
body { margin: 0px auto; font-family: "Segoe UI", Helvetica, sans-serif, Tahoma, Arial, Geneva, Georgia, Palatino, "Times New Roman", "Hiragino Sans GB", 冬青黑体, "Microsoft YaHei", 微软雅黑, "Microsoft YaHei UI", "WenQuanYi Micro Hei", 文泉驿雅黑, Dengxian, 等线体, STXihei, 华文细黑, "Liberation Sans", "Droid Sans", NSimSun, 新宋体, SimSun, 宋体; color: rgb(34, 34, 34); line-height: 1.5; padding: 15px; background: rgb(238, 238, 238); font-size: 16px; }
h1, h2, h3, h4, h5, h6 { color: rgb(34, 34, 34); font-weight: bold; margin-top: 20px; margin-bottom: 10px; padding: 0px; }
p { padding: 0px; margin-top: 16px; margin-bottom: 16px; }
h1 { font-size: 26px; }
h2 { font-size: 24px; }
h3 { font-size: 22px; }
h4 { font-size: 20px; }
h5 { font-size: 19px; }
h6 { font-size: 18px; }
a { color: rgb(0, 153, 255); margin: 0px; padding: 0px; vertical-align: baseline; text-decoration: none; word-break: break-word; }
a:hover { text-decoration: underline; color: rgb(255, 102, 0); }
a:visited { color: purple; }
ul, ol { padding: 0px 0px 0px 24px; }
li { line-height: 24px; }
li ul, li ol { margin-left: 16px; }
p, ul, ol { font-size: 16px; line-height: 24px; }
pre { display: block; overflow-y: hidden; overflow-x: auto; tab-size: 4; }
code { font-family: Consolas, Monaco, monospace, Courier; color: rgb(142, 36, 170); word-break: break-word; }
pre code { display: block; overflow-x: auto; padding: 0.5em; color: rgb(34, 34, 34); background-color: rgb(224, 224, 224); border-left: 0.5em solid rgb(0, 137, 123); line-height: 1.5; font-family: Consolas, Monaco, monospace, Courier; white-space: pre; tab-size: 4; }
pre code.markdown-metadata { border-left: 0.5em solid rgb(128, 203, 196); }
aside { display: block; float: right; width: 390px; }
blockquote { color: rgb(102, 102, 102); border-left: 0.5em solid rgb(122, 122, 122); padding: 0px 1em; margin-left: 0px; }
blockquote p { color: rgb(102, 102, 102); }
hr { display: block; text-align: left; margin: 1em 0px; border: none; height: 2px; background: rgb(153, 153, 153); }
table { padding: 0px; margin: 1rem 0.5rem; border-collapse: collapse; }
table tr { border-top: 2px solid rgb(204, 204, 204); background-color: white; margin: 0px; padding: 0px; }
table tr:nth-child(2n) { background-color: rgb(248, 248, 248); }
table tr th { font-weight: bold; border: 2px solid rgb(204, 204, 204); margin: 0px; padding: 6px 13px; }
table tr td { border: 2px solid rgb(204, 204, 204); margin: 0px; padding: 6px 13px; }
table tr th :first-child, table tr td :first-child { margin-top: 0px; }
table tr th :last-child, table tr td :last-child { margin-bottom: 0px; }
div.mermaid-diagram { margin: 16px 0px; overflow-y: hidden; }
div.flowchart-diagram { padding: 0px 5px; margin: 16px 0px; width: fit-content; overflow: hidden; }
div.wavedrom-diagram { padding: 0px 5px; margin: 16px 0px; width: fit-content; overflow: hidden; }
div.plantuml-diagram { padding: 5px 5px 0px; margin: 16px 0px; width: fit-content; overflow: hidden; }
.img-package { text-align: center; }
img.img-center { display: block; margin-left: auto; margin-right: auto; }
span.img-caption { min-width: 20%; max-width: 80%; display: inline-block; padding: 10px; margin: 0px auto; border-bottom: 1px solid rgb(192, 192, 192); color: rgb(108, 108, 108); text-align: center; line-height: 1.5; }
.emoji_zero, .emoji_one, .emoji_two, .emoji_three, .emoji_four, .emoji_five, .emoji_six, .emoji_seven, .emoji_eight, .emoji_nine { margin-left: 5px; margin-right: 8px; }
div.preview-hint { opacity: 0.5; margin-top: 30%; margin-bottom: 30%; align-items: center; display: flex; flex-direction: column; justify-content: center; }
table.hljs-ln tr { border: none; background-color: transparent; }
table.hljs-ln tr td { border: none; background-color: transparent; }
table.hljs-ln tr td.hljs-ln-numbers { user-select: none; text-align: center; color: rgb(170, 170, 170); border-right: 1px solid rgb(204, 204, 204); vertical-align: top; padding-right: 5px; white-space: nowrap; }
table.hljs-ln tr td.hljs-ln-code { padding-left: 10px; }
::-webkit-scrollbar { background-color: rgb(234, 234, 234); width: 14px; height: 14px; border: none; }
::-webkit-scrollbar-corner { background-color: rgb(234, 234, 234); }
::-webkit-scrollbar-button { height: 14px; width: 14px; background-color: rgb(234, 234, 234); }
::-webkit-scrollbar-button:hover { background-color: rgb(208, 208, 208); }
::-webkit-scrollbar-button:active { background-color: rgb(178, 178, 178); }
::-webkit-scrollbar-track { background-color: rgb(234, 234, 234); }
::-webkit-scrollbar-thumb { border: none; background-color: rgb(218, 218, 218); }
::-webkit-scrollbar-thumb:hover { background-color: rgb(208, 208, 208); }
::-webkit-scrollbar-thumb:active { background-color: rgb(178, 178, 178); }
::-webkit-scrollbar-button:horizontal:increment { background-image: url('data:image/svg+xml;utf8,<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <g>    <g transform="rotate(-90 256.00000000000006,256) " id="svg_1">   <polygon fill="%23333333" id="svg_2" points="128,192 256,320 384,192  "/>  </g> </g></svg>'); background-repeat: no-repeat; background-size: contain; }
::-webkit-scrollbar-button:horizontal:decrement { background-image: url('data:image/svg+xml;utf8,<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <g>    <g transform="rotate(90 255.99999999999997,256.00000000000006) " id="svg_1">   <polygon points="128,192 256,320 384,192  " id="svg_2" fill="%23333333"/>  </g> </g></svg>'); background-repeat: no-repeat; background-size: contain; }
::-webkit-scrollbar-button:vertical:increment { background-image: url('data:image/svg+xml;utf8,<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <g>    <g transform="null" id="svg_1">   <polygon points="128,192 256,320 384,192  " id="svg_2" fill="%23333333"/>  </g> </g></svg>'); background-repeat: no-repeat; background-size: contain; }
::-webkit-scrollbar-button:vertical:decrement { background-image: url('data:image/svg+xml;utf8,<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <g>    <g transform="rotate(180 255.99999999999997,256) " id="svg_1">   <polygon points="128,192 256,320 384,192  " id="svg_2" fill="%23333333"/>  </g> </g></svg>'); background-repeat: no-repeat; background-size: contain; }
::selection { background: rgb(25, 118, 210); color: rgb(238, 238, 238); }
.modal-box { background-color: rgba(234, 234, 234, 0.952941); }
span.modal-close { color: rgb(102, 102, 102); }
span.modal-close:hover, span.modal-close:focus { color: rgb(34, 34, 34); }
.hljs { display: block; overflow-x: auto; padding: 0.5em; background: rgb(224, 224, 224); }
.hljs, .hljs-subst { color: rgb(54, 54, 54); }
.hljs-comment { color: rgb(118, 118, 118); }
.hljs-keyword, .hljs-attribute, .hljs-selector-tag, .hljs-meta-keyword, .hljs-doctag, .hljs-name { color: rgb(0, 0, 238); }
.hljs-type, .hljs-string, .hljs-number, .hljs-selector-id, .hljs-selector-class, .hljs-quote, .hljs-template-tag, .hljs-deletion { color: rgb(136, 0, 0); }
.hljs-title, .hljs-section { color: rgb(136, 0, 0); font-weight: bold; }
.hljs-regexp, .hljs-symbol, .hljs-variable, .hljs-template-variable, .hljs-link, .hljs-selector-attr, .hljs-selector-pseudo { color: rgb(188, 96, 96); }
.hljs-literal { color: rgb(175, 0, 215); }
.hljs-built_in, .hljs-bullet, .hljs-code, .hljs-addition { color: rgb(0, 135, 0); }
.hljs-meta { color: rgb(31, 113, 153); }
.hljs-meta-string { color: rgb(77, 153, 191); }
.hljs-emphasis { font-style: italic; }
.hljs-strong { font-weight: bold; }
.mermaid-diagram .mermaid .label { color: rgb(51, 51, 51); }
.mermaid-diagram .node rect, .mermaid-diagram .node circle, .mermaid-diagram .node ellipse, .mermaid-diagram .node polygon { fill: rgb(236, 236, 255); stroke: rgb(204, 204, 255); stroke-width: 1px; }
.mermaid-diagram .edgePath .path { stroke: rgb(51, 51, 51); }
.mermaid-diagram .edgeLabel { background-color: rgb(232, 232, 232); }
.mermaid-diagram .cluster rect { fill: rgb(255, 255, 222) !important; rx: 4 !important; stroke: rgb(170, 170, 51) !important; stroke-width: 1px !important; }
.mermaid-diagram .cluster text { fill: rgb(51, 51, 51); }
.mermaid-diagram .actor { stroke: rgb(204, 204, 255); fill: rgb(236, 236, 255); }
.mermaid-diagram text.actor { fill: black; stroke: none; }
.mermaid-diagram .actor-line { stroke: grey; }
.mermaid-diagram .messageLine0 { stroke-width: 1.5; stroke: rgb(51, 51, 51); }
.mermaid-diagram .messageLine1 { stroke-width: 1.5; stroke: rgb(51, 51, 51); }
.mermaid-diagram #arrowhead { fill: rgb(51, 51, 51); }
.mermaid-diagram #crosshead path { fill: rgb(51, 51, 51) !important; stroke: rgb(51, 51, 51) !important; }
.mermaid-diagram .messageText { fill: rgb(51, 51, 51); stroke: none; }
.mermaid-diagram .labelBox { stroke: rgb(204, 204, 255); fill: rgb(236, 236, 255); }
.mermaid-diagram .labelText { fill: black; stroke: none; }
.mermaid-diagram .loopText { fill: black; stroke: none; }
.mermaid-diagram .loopLine { stroke-width: 2; stroke: rgb(204, 204, 255); }
.mermaid-diagram .note { stroke: rgb(170, 170, 51); fill: rgb(255, 245, 173); }
.mermaid-diagram .noteText { fill: black; stroke: none; font-family: "trebuchet ms", verdana, arial; font-size: 14px; }
.mermaid-diagram .section { stroke: none; opacity: 0.2; }
.mermaid-diagram .section0 { fill: rgba(102, 102, 255, 0.490196); }
.mermaid-diagram .section2 { fill: rgb(255, 244, 0); }
.mermaid-diagram .section1, .mermaid-diagram .section3 { fill: white; opacity: 0.2; }
.mermaid-diagram .sectionTitle0 { fill: rgb(51, 51, 51); }
.mermaid-diagram .sectionTitle1 { fill: rgb(51, 51, 51); }
.mermaid-diagram .sectionTitle2 { fill: rgb(51, 51, 51); }
.mermaid-diagram .sectionTitle3 { fill: rgb(51, 51, 51); }
.mermaid-diagram .sectionTitle { text-anchor: start; font-size: 11px; }
.mermaid-diagram .grid .tick { stroke: lightgrey; opacity: 0.3; shape-rendering: crispEdges; }
.mermaid-diagram .grid path { stroke-width: 0; }
.mermaid-diagram .today { fill: none; stroke: red; stroke-width: 2px; }
.mermaid-diagram .task { stroke-width: 2; }
.mermaid-diagram .taskText { text-anchor: middle; font-size: 11px; }
.mermaid-diagram .taskTextOutsideRight { fill: black; text-anchor: start; font-size: 11px; }
.mermaid-diagram .taskTextOutsideLeft { fill: black; text-anchor: end; font-size: 11px; }
.mermaid-diagram .taskText0, .mermaid-diagram .taskText1, .mermaid-diagram .taskText2, .mermaid-diagram .taskText3 { fill: white; }
.mermaid-diagram .task0, .mermaid-diagram .task1, .mermaid-diagram .task2, .mermaid-diagram .task3 { fill: rgb(138, 144, 221); stroke: rgb(83, 79, 188); }
.mermaid-diagram .taskTextOutside0, .mermaid-diagram .taskTextOutside2 { fill: black; }
.mermaid-diagram .taskTextOutside1, .mermaid-diagram .taskTextOutside3 { fill: black; }
.mermaid-diagram .active0, .mermaid-diagram .active1, .mermaid-diagram .active2, .mermaid-diagram .active3 { fill: rgb(191, 199, 255); stroke: rgb(83, 79, 188); }
.mermaid-diagram .activeText0, .mermaid-diagram .activeText1, .mermaid-diagram .activeText2, .mermaid-diagram .activeText3 { fill: black !important; }
.mermaid-diagram .done0, .mermaid-diagram .done1, .mermaid-diagram .done2, .mermaid-diagram .done3 { stroke: grey; fill: lightgrey; stroke-width: 2; }
.mermaid-diagram .doneText0, .mermaid-diagram .doneText1, .mermaid-diagram .doneText2, .mermaid-diagram .doneText3 { fill: black !important; }
.mermaid-diagram .crit0, .mermaid-diagram .crit1, .mermaid-diagram .crit2, .mermaid-diagram .crit3 { stroke: rgb(255, 136, 136); fill: red; stroke-width: 2; }
.mermaid-diagram .activeCrit0, .mermaid-diagram .activeCrit1, .mermaid-diagram .activeCrit2, .mermaid-diagram .activeCrit3 { stroke: rgb(255, 136, 136); fill: rgb(191, 199, 255); stroke-width: 2; }
.mermaid-diagram .doneCrit0, .mermaid-diagram .doneCrit1, .mermaid-diagram .doneCrit2, .mermaid-diagram .doneCrit3 { stroke: rgb(255, 136, 136); fill: lightgrey; stroke-width: 2; cursor: pointer; shape-rendering: crispEdges; }
.mermaid-diagram .doneCritText0, .mermaid-diagram .doneCritText1, .mermaid-diagram .doneCritText2, .mermaid-diagram .doneCritText3 { fill: black !important; }
.mermaid-diagram .activeCritText0, .mermaid-diagram .activeCritText1, .mermaid-diagram .activeCritText2, .mermaid-diagram .activeCritText3 { fill: black !important; }
.mermaid-diagram .titleText { text-anchor: middle; font-size: 18px; fill: black; }
.mermaid-diagram .node text { font-family: "trebuchet ms", verdana, arial; font-size: 14px; }
.mermaid-diagram div.mermaidTooltip { position: absolute; text-align: center; max-width: 200px; padding: 2px; font-family: "trebuchet ms", verdana, arial; font-size: 12px; background: rgb(255, 255, 222); border: 1px solid rgb(170, 170, 51); border-radius: 2px; pointer-events: none; z-index: 100; }
#mermaid-diagram-1 .node > rect { }
#mermaid-diagram-1 .node text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-1 .edgeLabel text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-1 .cluster rect { fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-1 .cyan > rect, .cyan > polygon, .cyan > circle, .cyan > ellipse { fill: rgb(153, 255, 255); stroke: rgb(255, 255, 255); }
#mermaid-diagram-2 .node > rect { }
#mermaid-diagram-2 .node text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-2 .edgeLabel text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-2 .cluster rect { fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-2 .cyan > rect, .cyan > polygon, .cyan > circle, .cyan > ellipse { fill: rgb(153, 255, 255); stroke: rgb(255, 255, 255); }
#mermaid-diagram-3 .node > rect { }
#mermaid-diagram-3 .node text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-3 .edgeLabel text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-3 .cluster rect { fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-3 .cyan > rect, .cyan > polygon, .cyan > circle, .cyan > ellipse { fill: rgb(153, 255, 255); stroke: rgb(255, 255, 255); }
#mermaid-diagram-4 .node > rect { }
#mermaid-diagram-4 .node text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-4 .edgeLabel text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-4 .cluster rect { fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-4 .cyan > rect, .cyan > polygon, .cyan > circle, .cyan > ellipse { fill: rgb(153, 255, 255); stroke: rgb(255, 255, 255); }
#mermaid-diagram-5 .node > rect { }
#mermaid-diagram-5 .node text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-5 .edgeLabel text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-5 .cluster rect { fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-5 .cyan > rect, .cyan > polygon, .cyan > circle, .cyan > ellipse { fill: rgb(153, 255, 255); stroke: rgb(255, 255, 255); }
#mermaid-diagram-6 .node > rect { }
#mermaid-diagram-6 .node text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-6 .edgeLabel text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-6 .cluster rect { fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-6 .cyan > rect, .cyan > polygon, .cyan > circle, .cyan > ellipse { fill: rgb(153, 255, 255); stroke: rgb(255, 255, 255); }
#mermaid-diagram-7 .node > rect { }
#mermaid-diagram-7 .node text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-7 .edgeLabel text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-7 .cluster rect { fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-7 .cyan > rect, .cyan > polygon, .cyan > circle, .cyan > ellipse { fill: rgb(153, 255, 255); stroke: rgb(255, 255, 255); }
#mermaid-diagram-8 .node > rect { }
#mermaid-diagram-8 .node text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-8 .edgeLabel text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-8 .cluster rect { fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-8 .cyan > rect, .cyan > polygon, .cyan > circle, .cyan > ellipse { fill: rgb(153, 255, 255); stroke: rgb(255, 255, 255); }
#mermaid-diagram-9 .node > rect { }
#mermaid-diagram-9 .node text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-9 .edgeLabel text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-9 .cluster rect { fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-9 .cyan > rect, .cyan > polygon, .cyan > circle, .cyan > ellipse { fill: rgb(153, 255, 255); stroke: rgb(255, 255, 255); }
#mermaid-diagram-10 .node > rect { }
#mermaid-diagram-10 .node text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-10 .edgeLabel text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-10 .cluster rect { fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-10 .cyan > rect, .cyan > polygon, .cyan > circle, .cyan > ellipse { fill: rgb(153, 255, 255); stroke: rgb(255, 255, 255); }

    </style>

    <script type="text/javascript">
var toc = [];

var setVisible = function(node, visible) {
    var cl = 'hide-none';
    if (visible) {
        node.classList.remove(cl);
    } else {
        node.classList.add(cl);
    }
};

var isVisible = function(node) {
    var cl = 'hide-none';
    return !node.classList.contains(cl);
};

var setPostContentExpanded = function(node, expanded) {
    var cl = 'col-expand';
    if (expanded) {
        node.classList.add(cl);
    } else {
        node.classList.remove(cl);
    }
};

var setOutlinePanelVisible = function(visible) {
    var outlinePanel = document.getElementById('outline-panel');
    var postContent = document.getElementById('post-content');

    setVisible(outlinePanel, visible);
    setPostContentExpanded(postContent, !visible);
};

var isOutlinePanelVisible = function() {
    var outlinePanel = document.getElementById('outline-panel');
    return isVisible(outlinePanel);
};

window.addEventListener('load', function() {
    var outlinePanel = document.getElementById('outline-panel');
    outlinePanel.style.display = 'initial';

    var floatingContainer = document.getElementById('container-floating');
    floatingContainer.style.display = 'initial';

    var outlineContent = document.getElementById('outline-content');
    var postContent = document.getElementById('post-content');

    // Escape @text to Html.
    var escapeHtml = function(text) {
        var map = {
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;',
            '"': '&quot;',
            "'": '&#039;'
        };

        return text.replace(/[&<>"']/g, function(m) { return map[m]; });
    }

    // Fetch the outline.
    var headers = postContent.querySelectorAll("h1, h2, h3, h4, h5, h6");
    toc = [];
    for (var i = 0; i < headers.length; ++i) {
        var header = headers[i];

        toc.push({
            level: parseInt(header.tagName.substr(1)),
            anchor: header.id,
            title: escapeHtml(header.textContent)
        });
    }

    if (toc.length == 0) {
        setOutlinePanelVisible(false);
        setVisible(floatingContainer, false);
        return;
    }

    var baseLevel = baseLevelOfToc(toc);
    var tocTree = tocToTree(toPerfectToc(toc, baseLevel), baseLevel);

    outlineContent.innerHTML = tocTree;
    setOutlinePanelVisible(true);
    setVisible(floatingContainer, true);
});

// Return the topest level of @toc, starting from 1.
var baseLevelOfToc = function(p_toc) {
    var level = -1;
    for (i in p_toc) {
        if (level == -1) {
            level = p_toc[i].level;
        } else if (level > p_toc[i].level) {
            level = p_toc[i].level;
        }
    }

    if (level == -1) {
        level = 1;
    }

    return level;
};

// Handle wrong title levels, such as '#' followed by '###'
var toPerfectToc = function(p_toc, p_baseLevel) {
    var i;
    var curLevel = p_baseLevel - 1;
    var perfToc = [];
    for (i in p_toc) {
        var item = p_toc[i];

        // Insert empty header.
        while (item.level > curLevel + 1) {
            curLevel += 1;
            var tmp = { level: curLevel,
                        anchor: '',
                        title: '[EMPTY]'
                      };
            perfToc.push(tmp);
        }

        perfToc.push(item);
        curLevel = item.level;
    }

    return perfToc;
};

var itemToHtml = function(item) {
    return '<a href="#' + item.anchor + '" data="' + item.anchor + '">' + item.title + '</a>';
};

// Turn a perfect toc to a tree using <ul>
var tocToTree = function(p_toc, p_baseLevel) {
    var i;
    var front = '<li>';
    var ending = ['</li>'];
    var curLevel = p_baseLevel;
    for (i in p_toc) {
        var item = p_toc[i];
        if (item.level == curLevel) {
            front += '</li>';
            front += '<li>';
            front += itemToHtml(item);
        } else if (item.level > curLevel) {
            // assert(item.level - curLevel == 1)
            front += '<ul>';
            ending.push('</ul>');
            front += '<li>';
            front += itemToHtml(item);
            ending.push('</li>');
            curLevel = item.level;
        } else {
            while (item.level < curLevel) {
                var ele = ending.pop();
                front += ele;
                if (ele == '</ul>') {
                    curLevel--;
                }
            }
            front += '</li>';
            front += '<li>';
            front += itemToHtml(item);
        }
    }
    while (ending.length > 0) {
        front += ending.pop();
    }
    front = front.replace("<li></li>", "");
    front = '<ul>' + front + '</ul>';
    return front;
};

var toggleMore = function() {
    if (toc.length == 0) {
        return;
    }

    var p = document.getElementById('floating-more');
    if (isOutlinePanelVisible()) {
        p.textContent = '<';
        setOutlinePanelVisible(false);
    } else {
        p.textContent = '>';
        setOutlinePanelVisible(true);
    }
};

window.addEventListener('scroll', function() {
    if (toc.length == 0 || !isOutlinePanelVisible()) {
        return;
    }

    var postContent = document.getElementById('post-content');
    var scrollTop = document.documentElement.scrollTop
                    || document.body.scrollTop
                    || window.pageYOffset;
    var eles = postContent.querySelectorAll("h1, h2, h3, h4, h5, h6");

    if (eles.length == 0) {
        return;
    }

    var idx = -1;
    var biaScrollTop = scrollTop + 50;
    for (var i = 0; i < eles.length; ++i) {
        if (biaScrollTop >= eles[i].offsetTop) {
            idx = i;
        } else {
            break;
        }
    }

    var header = '';
    if (idx != -1) {
        header = eles[idx].id;
    }

    highlightItemOnlyInOutline(header);
});

var highlightItemOnlyInOutline = function(id) {
    var cl = 'outline-bold';
    var outlineContent = document.getElementById('outline-content');
    var eles = outlineContent.querySelectorAll("a");
    var target = null;
    for (var i = 0; i < eles.length; ++i) {
        var ele = eles[i];
        if (ele.getAttribute('data') == id) {
            target = ele;
            ele.classList.add(cl);
        } else {
            ele.classList.remove(cl);
        }
    }

    // TODO: scroll target into view within the outline panel scroll area.
};

</script>


<!-- HEAD_PLACE_HOLDER -->
</head>
<body>
<div class="container-fluid">
<div class="row flex-xl-nowrap">
    <div id="outline-panel" style="display:none;" class="d-none d-md-block d-xl-block col-md-3 col-xl-2 bd-toc">
        <div id="outline-content" class="section-nav"></div>
    </div>
    <div id="post-content" class="col-12 col-md-9 col-xl-10 py-md-3 pl-md-5 bd-content">
    <div style="page-break-after: always;"></div>
<h1 id="toc_0">5. 对象关系映射（ORM）<a class="vnote-anchor" href="#toc_0" data-anchor-icon="#"></a></h1>
<p><img src='' alt="采用mORMot" class="view-image"></p>
<p>  框架的ORM部分主要在<code>mORMot.pas</code>单元中实现（参见<a href="">对象关系映射(ORM)</a>），并通过<code>mORMotSQLite3.pas</code>、<code>mORMotDB.pas</code>、<code>SynSQLite3.pas</code>、<code>SynDB.pas</code>等单元来访问各数据库后端。</p>
<p>  对数据的访问是通过定义从TSQLRecord派生的、类似于Delphi中的高级对象来实现。</p>
<p>  在客户端-服务端ORM中，这些TSQLRecord类主要用于三个目的：</p>
<ul>
<li>通过数据库引擎存储和检索数据。通常，您都可以忘记编写SQL查询，数据库的CRUD（<code>SELECT</code>/<code>INSERT</code>/<code>UPDATE</code>/<code>DELETE</code>）操作，都是通过mORMot的ORM核心动态创建的；对非SQL引擎，如MongoDB，也是同样的；</li>
<li>以RESTful的方式，让客户端和服务端都可以访问业务逻辑对象；</li>
<li>用适当的字段类型填充表格内容（如转换为适当的表头名称，枚举以文本形式显示，布尔值用复选框来呈现）；直接通过字段定义来创建菜单、报表；自动生成编辑窗。</li>
</ul>
<p>  我们的ORM引擎拥有诸多高级功能，如约定优于配置，集成安全性，本地或远程访问，REST JSON发布（用于AJAX或移动客户端），直接数据库访问（绕过低效的<code>DB.pas</code>单元），内存中的内容缓存，可选的审计跟踪（变更跟踪），以及与框架的其他部分（如SOA、日志记录、身份验证等）集成。</p>
<h2 id="toc_1">5.1. TSQLRecord字段定义<a class="vnote-anchor" href="#toc_1" data-anchor-icon="#"></a></h2>
<p>  框架的所有ORM处理都依赖于<code>TSQLRecord</code>类，这个抽象的<code>TSQLRecord</code>类拥有许多内置方法，支持以通用、便捷的方式在记录级完成大部分ORM处理。</p>
<p>  首先定义了一个<code>ID: TID</code>主键字段，在<code>mORMot.pas</code>中定义为<code>Int64</code>。</p>
<pre><code class="lang-pascal hljs">    <span class="hljs-keyword">type</span>
      TID = <span class="hljs-keyword">type</span> Int64;
      ...
      <span class="hljs-title">TSQLRecord</span> = <span class="hljs-keyword">class</span>(TObject)
      ...
        <span class="hljs-keyword">property</span> ID: TID <span class="hljs-keyword">read</span> GetID <span class="hljs-keyword">write</span> fID;
      ...
</code></pre>
<p>  实际上，我们的ORM基于<code>Int64</code>主键，与SQLite3的<code>ID</code>/<code>RowID</code>主键一致。</p>
<p>  您可能会对这个限制感到失望，但这是实现SQLite3虚表所必需的，这里我们不会辩论复合主键（即多个字段）问题，这对ORM来说不是一个好主意。在您以前的RDBMS数据建模中，您可能定义一个TEXT主键，或一个GUID主键，这些类型的主键效率低于INTEGER，从ORM内部实现来看，也过于复杂。如果需要，您可以将<code>string</code>、<code>TGUID</code>等字段，使用后面所述的<code>stored AS_UNIQUE</code>属性将其定义成次关键字。</p>
<p>  所有TSQLRecord派生类的发布属性，都可以在客户端-服务端RESTful架构中通过RTTI方式访问。</p>
<p>  如用Delphi代码定义如下数据库<code>Baby</code>表:</p>
<pre><code class="lang-pascal hljs">  <span class="hljs-comment">/// some enumeration</span>
  <span class="hljs-comment">// - will be written as 'Female' or 'Male' in our UI Grid</span>
  <span class="hljs-comment">// - will be stored as its ordinal value, i.e. 0 for sFemale, 1 for sMale</span>
  <span class="hljs-comment">// - as you can see, ladies come first, here</span>
  TSex = (sFemale, sMale);
  
  <span class="hljs-comment">/// table used for the Babies queries</span>
  <span class="hljs-title">TSQLBaby</span> = <span class="hljs-keyword">class</span>(TSQLRecord)
    <span class="hljs-keyword">private</span>
      fName: RawUTF8;
      fAddress: RawUTF8;
      fBirthDate: TDateTime;
      fSex: TSex;
    <span class="hljs-keyword">published</span>
      <span class="hljs-keyword">property</span> <span class="hljs-keyword">Name</span>: RawUTF8 <span class="hljs-keyword">read</span> fName <span class="hljs-keyword">write</span> fName;
      <span class="hljs-keyword">property</span> Address: RawUTF8 <span class="hljs-keyword">read</span> fAddress <span class="hljs-keyword">write</span> fAddress;
      <span class="hljs-keyword">property</span> BirthDate: TDateTime <span class="hljs-keyword">read</span> fBirthDate <span class="hljs-keyword">write</span> fBirthDate;
      <span class="hljs-keyword">property</span> Sex: TSex <span class="hljs-keyword">read</span> fSex <span class="hljs-keyword">write</span> fSex;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  将这个<code>TSQLBaby</code>类添加到客户端和服务端的<code>TSQLModel</code>实例中，框架就会调用数据库引擎创建相应的Baby表（SQLite3本地数据库或其它外部数据库），只需编写Pascal代码，框架就会完成所有原本用SQL语句来完成的工作(如<code>CREATE TABLE…</code>)，包括所需的索引ORM都会创建好，与您编写SQL查询没有什么区别。</p>
<p>  ORM支持的发布属性类型如下，并可转换为特定数据库内容（如在SQLite3中，INTEGER为<code>Int64</code>，FLOAT为double，TEXT为UTF-8编码的文本)：</p>
<table>
<thead>
<tr>
<th>Delphi</th>
<th>SQLite3</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td>byte</td>
<td>INTEGER</td>
<td></td>
</tr>
<tr>
<td>word</td>
<td>INTEGER</td>
<td></td>
</tr>
<tr>
<td>integer</td>
<td>INTEGER</td>
<td></td>
</tr>
<tr>
<td>cardinal</td>
<td>INTEGER</td>
<td></td>
</tr>
<tr>
<td>Int64</td>
<td>INTEGER</td>
<td></td>
</tr>
<tr>
<td>boolean</td>
<td>INTEGER</td>
<td>0为假，其它为真</td>
</tr>
<tr>
<td>enumeration</td>
<td>INTEGER</td>
<td>存储枚举项的序号值（第一个元素从0开始）</td>
</tr>
<tr>
<td>set</td>
<td>INTEGER</td>
<td>每一位对应一个元素（因此，一个集合最多可以存储64个元素）</td>
</tr>
<tr>
<td>single</td>
<td>FLOAT</td>
<td></td>
</tr>
<tr>
<td>double</td>
<td>FLOAT</td>
<td></td>
</tr>
<tr>
<td>extended</td>
<td>FLOAT</td>
<td>存储为双精度类型（有精度损失）</td>
</tr>
<tr>
<td>currency</td>
<td>FLOAT</td>
<td>货币类型采用固定的小数位，实现了安全的转换，没有舍入误差</td>
</tr>
<tr>
<td>RawUTF8</td>
<td>TEXT</td>
<td>这是在ORM中存储文本内容的首选字段类型</td>
</tr>
<tr>
<td>WinAnsiString</td>
<td>TEXT</td>
<td>Delphi中的WinAnsi字符集（代码页1252）</td>
</tr>
<tr>
<td>RawUnicode</td>
<td>TEXT</td>
<td>Delphi中的UCS2字符集，同<code>AnsiString</code></td>
</tr>
<tr>
<td>WideString</td>
<td>TEXT</td>
<td>UCS2字符集，同COM BSTR类型（所有Delphi版本均使用Unicode编码）</td>
</tr>
<tr>
<td>SynUnicode</td>
<td>TEXT</td>
<td>是Delphi 2009以前的<code>WideString</code>类型，或后续版本的<code>UnicodeString</code>类型</td>
</tr>
<tr>
<td>string</td>
<td>TEXT</td>
<td>Delphi 2009之前无此类型（否则可能因转换丢失数据），<code>RawUTF8</code>在所有情况下都是首选</td>
</tr>
<tr>
<td>TDateTime</td>
<td>TEXT</td>
<td>ISO 8601日期时间编码格式，精度为秒</td>
</tr>
<tr>
<td>TDateTimeMS</td>
<td>TEXT</td>
<td>ISO 8601日期时间编码格式，精度为毫秒</td>
</tr>
<tr>
<td>TTimeLog</td>
<td>INTEGER</td>
<td>特有的<code>Int64</code>快速日期时间格式</td>
</tr>
<tr>
<td>TModTime</td>
<td>INTEGER</td>
<td>数据库记录被修改时的服务端日期时间（特有的<code>Int64</code>快速日期时间格式）</td>
</tr>
<tr>
<td>TCreateTime</td>
<td>INTEGER</td>
<td>数据库记录创建时的服务器端日期时间（特有的<code>Int64</code>快速日期时间格式）</td>
</tr>
<tr>
<td>TUnixTime</td>
<td>INTEGER</td>
<td>基于秒的Unix时间戳（即64位的自1970-01-01 00:00:00 UTC以来的秒数）</td>
</tr>
<tr>
<td><strong>TSQLRecord</strong></td>
<td>INTEGER</td>
<td>指向另一条记录的32位的<code>RowID</code>（注意：字段值包含的是一个<code>pointer(RowID)</code>，而不是一个有效的对象实例，对象实例必须通过使用<code>PtrInt(Field)</code>类型转换或检索字段的<code>ID</code>获取），或者使用<code>CreateJoin()</code>方法，Win64下是64位</td>
</tr>
<tr>
<td><strong>TID</strong></td>
<td>INTEGER</td>
<td>指向另一个记录的64位<code>RowID</code>，不与具体的表相关联</td>
</tr>
<tr>
<td><strong>TSQLRecordMany</strong></td>
<td>nothing</td>
<td>数据存储在一个单独的数据透视表中，这是<code>TSQLRecord</code>的一个特殊情况：它包含的不是<code>pointer(RowID)</code>，而是一个实例</td>
</tr>
<tr>
<td><strong>TRecordReference</strong> <strong>TRecordReferenceToBeDeleted</strong></td>
<td>INTEGER</td>
<td>用类似于<code>RecordRef</code>的Int64存储<code>ID</code>和<code>TSQLRecord</code>类类型，并在其引用的记录被删除时自动重置为0（对于<code>TRecordReference</code>）或删除该行（对于<code>TRecordReferencetobedeleted</code>），可引用数据库模型中任意表上的任意行</td>
</tr>
<tr>
<td>TSessionUserID</td>
<td>INTEGER</td>
<td>在<code>TSQLAuthUser</code>表中的64位<code>RowID</code>，记录当前有效登陆用户</td>
</tr>
<tr>
<td>TPersistent</td>
<td>TEXT</td>
<td>JSON对象（<code>ObjectToJSON</code>）</td>
</tr>
<tr>
<td>TCollection</td>
<td>TEXT</td>
<td>JSON对象数组（<code>ObjectToJSON</code>）</td>
</tr>
<tr>
<td>TObjectList</td>
<td>TEXT</td>
<td>JSON对象数组（<code>ObjectToJSON</code>），参见<code>TJSONSerializer.RegisterClassForJSON</code></td>
</tr>
<tr>
<td>TStrings</td>
<td>TEXT</td>
<td>字符串的JSON数组（<code>ObjectToJSON</code>）</td>
</tr>
<tr>
<td>TRawUTF8List</td>
<td>TEXT</td>
<td>字符串的JSON数组（<code>ObjectToJSON</code>）</td>
</tr>
<tr>
<td>any TObject</td>
<td>TEXT</td>
<td>参见<code>TJSONSerializer.RegisterCustomSerializer</code></td>
</tr>
<tr>
<td>TSQLRawBlob</td>
<td>BLOB</td>
<td><code>RawByteString</code>类型的别名</td>
</tr>
<tr>
<td>dynamic arrays</td>
<td>BLOB</td>
<td><code>TDynArray.SaveTo</code>二进制格式</td>
</tr>
<tr>
<td>variant</td>
<td>TEXT</td>
<td>JSON形式的数字或文本，或使用<code>TDocVariant</code>自定义JSON对象或数组</td>
</tr>
<tr>
<td>TNullableInteger</td>
<td>INTEGER</td>
<td>允许为空的Int64类型</td>
</tr>
<tr>
<td>TNullableBoolean</td>
<td>INTEGER</td>
<td>允许为空的布尔类型</td>
</tr>
<tr>
<td>TNullableFloat</td>
<td>FLOAT</td>
<td>允许为空的浮点类型</td>
</tr>
<tr>
<td>TNullableCurrency</td>
<td>FLOAT</td>
<td>允许为空的货币类型</td>
</tr>
<tr>
<td>TNullableDateTime</td>
<td>TEXT</td>
<td>允许为空的ISO 8601编码的日期时间类型</td>
</tr>
<tr>
<td>TNullableTimeLog</td>
<td>INTEGER</td>
<td>允许为空的<code>TTimeLog</code>类型</td>
</tr>
<tr>
<td>TNullableUTF8Text</td>
<td>TEXT</td>
<td>可空的UTF8文本类型</td>
</tr>
<tr>
<td>record</td>
<td>TEXT</td>
<td>JSON字符串或对象，Delphi XE5后续版本已支持，或通过重写<code>TSQLRecord</code>来定义，早期版本请使用<code>InternalRegisterCustomProperties</code></td>
</tr>
<tr>
<td>TRecordVersion</td>
<td>INTEGER</td>
<td>64位版本号，在每次修改对象时都将更新，用于远程同步</td>
</tr>
</tbody>
</table>
<h3 id="toc_2">5.1.1. property特性<a class="vnote-anchor" href="#toc_2" data-anchor-icon="#"></a></h3>
<p>  <strong><em>译注：Published property：译为“发布属性”</em></strong></p>
<p>  发布字段定义时可为其附加一些特性：</p>
<ul>
<li>如果标记为<code>stored AS_UNIQUE</code>，则会在数据库中保持创建记录的唯一性（为该属性字段创建SQL索引，并在插入、更新时检查值的唯一性）；</li>
<li>在<code>TSQLRecord</code>中定义的动态数组字段，可附加<code>index</code>值，并使用<code>DynArray</code>（<code>DynArrayFieldIndex</code>）方法创建动态数组；</li>
<li>对于外部存储的<code>RawUTF8</code>/<code>string</code>/<code>WideString</code>/<code>WinAnsiString</code>字段，即存储在远程<code>SynDB</code>中的文本字段，在数据库中创建相应的字段时（SQLite3或PostgreSQL无该限制），<code>index</code>将用于定义该字段的最大字符数。</li>
</ul>
<p>  例如，下面的类为<code>SerialNumber</code>字段属性附加了<code>index</code>值，(即使用外部数据库时最长可达30个字符)，以及<code>diaper</code>（<code>TSQLDiaperModel</code>）和使用它的<code>baby</code>（<code>TSQLBaby</code>）的引用字段。由于<code>TSQLRecord</code>派生表中始终存在<code>ID</code>/<code>RowID</code>字段，因此在本例中，您能够通过<code>ID</code>或<code>SerialNumber</code>快速查找到特定的<code>diaper</code>。</p>
<pre><code class="lang-pascal hljs"><span class="hljs-comment">/// table used for the Diaper queries</span>
<span class="hljs-title">TSQLDiaper</span> = <span class="hljs-keyword">class</span>(TSQLRecord)
  <span class="hljs-keyword">private</span>
    fSerialNumber: RawUTF8;
    fModel: TSQLDiaperModel;
    fBaby: TSQLBaby;
  <span class="hljs-keyword">published</span>
    <span class="hljs-keyword">property</span> SerialNumber: RawUTF8 <span class="hljs-keyword">index</span> <span class="hljs-number">30</span> <span class="hljs-keyword">read</span> fSerialNumber <span class="hljs-keyword">write</span> fSerialNumber <span class="hljs-keyword">stored</span> AS_UNIQUE;
    <span class="hljs-keyword">property</span> Model: TSQLDiaperModel <span class="hljs-keyword">read</span> fModel <span class="hljs-keyword">write</span> fModel;
    <span class="hljs-keyword">property</span> Baby: TSQLBaby <span class="hljs-keyword">read</span> fBaby <span class="hljs-keyword">write</span> fBaby;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>   注意，<code>TTNullableUTF8Text</code>属性也遵循相同的<code>index</code>附加特性。</p>
<h3 id="toc_3">5.1.2. Text字段<a class="vnote-anchor" href="#toc_3" data-anchor-icon="#"></a></h3>
<p>    事实上，如果您使用Unicode之前的Delphi版本处理<code>string</code>类型（在之前版本中<code>string = AnsiString</code>，并使用系统编码页，Delphi 2009及以后版本使用<code>UnicodeString</code>），您可能会丢失一些内容，所以我们不推荐这样用。</p>
<p>    在我们的框架中，文本类型的存储建议使用<code>RawUTF8</code>，以取代<a href="">Unicode和UTF-8</a>。所有业务流程最好都使用<code>RawUTF8</code>变量和方法（<code>syncommon.pas</code>中有全部所需函数），然后使用<code>mORMoti18n</code>中的U2S/S2U显式地将<code>RawUTF8</code>转换为字符串，或使用<code>StringToUTF8</code>/<code>UTF8ToString</code>，它能根据当前的i18n设置正确完成字符集转换。<code>Unicode</code>版本的Delphi（从Delphi 2009开始），可以直接将<code>string</code>/<code>UnicodeString</code>值赋给一个<code>RawUTF8</code>，或从<code>RawUTF8</code>获取值，但这种隐式转换比<code>StringToUTF8</code>/<code>UTF8ToString</code>函数要慢，对于<code>unicode</code>之前的Delphi版本（Delphi 2007及之前），这种直接的赋值可能会使所有非7位ASCII字符数据丢失，因此应当显式地调用<code>StringToUTF8</code>/<code>UTF8ToString</code>函数。</p>
<p>   所有RawUTF8相关的底层处理函数和类都在SynCommons.pas单元中，以替代SysUtils.pas的功能，mORMot核心实现针对RawUTF8在速度、多线程方面进行了优化，所以建议不要在代码中使用字符串，除非涉及到VCL和用户界面层。</p>
<div class="mermaid-diagram"><svg id="mermaid-diagram-1" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 347 386" style="max-width:347px;"><style type="text/css" title="mermaid-svg-internal-css">/*  */
#mermaid-diagram-1 .node&gt;rect { ; }
#mermaid-diagram-1 .node text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-1 .edgeLabel text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-1 .cluster rect  { rx:4px; fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-1 .cyan&gt;rect, .cyan&gt;polygon, .cyan&gt;circle, .cyan&gt;ellipse { fill:#9ff;  stroke:#fff; }
/*  */
</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath" style="opacity: 1;"><path class="path" d="M96,64L96,89L96,114" marker-end="url(#arrowhead20)" style="fill:none"></path><defs><marker id="arrowhead20" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M96,158L96,183L96,208" marker-end="url(#arrowhead21)" style="fill:none"></path><defs><marker id="arrowhead21" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M96,252L96,277L96,302" marker-end="url(#arrowhead22)" style="fill:none"></path><defs><marker id="arrowhead22" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M264.5,64L264.5,89L264.5,114" marker-end="url(#arrowhead23)" style="fill:none"></path><defs><marker id="arrowhead23" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M264.5,158L264.5,183L264.5,208" marker-end="url(#arrowhead24)" style="fill:none"></path><defs><marker id="arrowhead24" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M264.5,252L264.5,277L264.5,302" marker-end="url(#arrowhead25)" style="fill:none"></path><defs><marker id="arrowhead25" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node cyan" id="A" transform="translate(96,42)" style="opacity: 1;"><rect rx="0" ry="0" x="-69.5" y="-22" width="139" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-59.5,-12)"><foreignObject width="119" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Presentation Tier</div></foreignObject></g></g></g><g class="node cyan" id="B" transform="translate(96,136)" style="opacity: 1;"><rect rx="0" ry="0" x="-65.5" y="-22" width="131" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-55.5,-12)"><foreignObject width="111" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Application Tier</div></foreignObject></g></g></g><g class="node cyan" id="C" transform="translate(96,230)" style="opacity: 1;"><rect rx="0" ry="0" x="-76" y="-22" width="152" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-66,-12)"><foreignObject width="132" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Business Logic Tier</div></foreignObject></g></g></g><g class="node cyan" id="D" transform="translate(96,324)" style="opacity: 1;"><rect rx="0" ry="0" x="-42" y="-22" width="84" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-12)"><foreignObject width="64" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Data Tier</div></foreignObject></g></g></g><g class="node cyan" id="A2" transform="translate(264.5,42)" style="opacity: 1;"><rect rx="0" ry="0" x="-30" y="-22" width="60" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-20,-12)"><foreignObject width="40" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">string</div></foreignObject></g></g></g><g class="node cyan" id="B2" transform="translate(264.5,136)" style="opacity: 1;"><rect rx="0" ry="0" x="-42.5" y="-22" width="85" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32.5,-12)"><foreignObject width="65" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">RawUTF8</div></foreignObject></g></g></g><g class="node cyan" id="C2" transform="translate(264.5,230)" style="opacity: 1;"><rect rx="0" ry="0" x="-42.5" y="-22" width="85" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32.5,-12)"><foreignObject width="65" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">RawUTF8</div></foreignObject></g></g></g><g class="node cyan" id="D2" transform="translate(264.5,324)" style="opacity: 1;"><rect rx="0" ry="0" x="-42.5" y="-22" width="85" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32.5,-12)"><foreignObject width="65" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">RawUTF8</div></foreignObject></g></g></g></g></g></g></svg></div>
<p>   有关在框架中处理UTF-8的更多信息，请参阅<a href="">Unicode和UTF-8</a>。</p>
<h3 id="toc_4">5.1.3. Date和time字段<a class="vnote-anchor" href="#toc_4" data-anchor-icon="#"></a></h3>
<p>   Delphi的<code>TDateTime</code>和<code>TDateTimeMS</code>属性是按ISO 8601标准以文本的形式存储在数据库中，精度分别为秒和毫秒。详细信息请参阅<a href="">Iso8601时间和日期</a>。</p>
<p>   <code>TTimeLog</code>/<code>TModTime</code>/<code>TCreateTime</code>提供了专有的<code>Int64</code>的日期时间替代方案，使用<code>SynCommons.pas</code>中定义的<code>TTimeLogBits</code>记录类型。</p>
<p>   这种格式在日期比较或文本互转时更快，并将作为整数存储在数据库中，因此它比普通的ISO 8601标准的<code>TDateTime</code>字段更高效。</p>
<p>   <code>TModTime</code>、<code>TCreateTime</code>与<code>TTimeLog</code>可以相互转换，ORM只是区别处理它们，以便在<code>TSQLRecord</code>记录修改时（<code>TModTime</code>）或记录创建时（<code>TCreateTime</code>），用当前UTC时间戳更新它们关联的字段值。存储的时间值是当前REST服务器返回的UTC时间戳，实际上，当任何REST客户端发起连接时，都将获取REST服务端的时间偏移量，确保所有客户端存储时间的一致性。</p>
<p>  也可以定义一个<code>TUnixTime</code>字段，用整数来记录UTC时间自1970-01-01 00:00:00以来的秒数，并序列化为64位JSON数字。这种编码的优点是可以用SQlite3日期/时间函数处理，并可以与大多数编程语言交互。</p>
<h3 id="toc_5">5.1.4. TSessionUserID字段<a class="vnote-anchor" href="#toc_5" data-anchor-icon="#"></a></h3>
<p>  如果定义了一个<code>TSessionUserID</code>字段，那么在<code>TSQLRecord</code>记录创建或修改时，这个字段会记录当前活动会话的<code>TSQLAuthUser.ID</code>，如果没有开启活动会话，则存储0。</p>
<p>  与<code>TModTime</code>字段相似，应该使用ORM的PUT/POST CRUD方法来处理该字段值，不要使用SQL语句（如<code>UPDATE Table SET Column=0</code>）去设置其内容。另外，客户端需先完成<code>TSessionUserID</code>字段的设置才能向服务端发送数据，Delphi和跨平台的ORM客户端均是如此。</p>
<h3 id="toc_6">5.1.5. Enumeration字段<a class="vnote-anchor" href="#toc_6" data-anchor-icon="#"></a></h3>
<p>  枚举会映射为整数进行处理，可使用<code>ord(aEnumValue)</code>或<code>TEnum(aIntegerValue)</code>互转。</p>
<p>  枚举映射为整数，有<code>byte</code>/<code>word</code>/<code>integer</code>几种类型，根据集合中元素的数量而定，例如，<code>byte(aSetValue)</code>最多8个元素，<code>word(aSetValue)</code>最多16个元素，<code>integer(aSetValue)</code>最多32个元素。</p>
<h3 id="toc_7">5.1.6. Floating point和Currency字段<a class="vnote-anchor" href="#toc_7" data-anchor-icon="#"></a></h3>
<p>  对于标准的浮点数，框架具备双精度类型和货币类型的处理能力。</p>
<p>  实际上，双精度类型是大多数数据库都直接支持的类型，也是新的CPU的SSE指令集能直接处理的类型（Delphi XE 2在64位模式下支持）；支持<code>extended</code>数值也没有有问题（如果需要，可以优先考虑使用专门的数学类来实现，而不是考虑用数据库来实现），也可以使用TEXT字段（或由动态数组映射的BLOB方式）实现对所需精度数值的处理。</p>
<p>  货币类型是Delphi存储和处理货币值的标准类型，也是x87 FPU指令集支持的类型，货币类型嘛，为“富人的世界”提供这样的专用数据类型也是值得的，货币类型精确到4个小数位，以避免出现四舍五入问题，支持从-922337203685477.5808到922337203685477.5807范围的货币运算，足够用于管理你口袋里的钱了。</p>
<p>  正如Delphi官方文件所描述的：</p>
<p>  货币类型是一种固定小数位数据类型，可以最小化货币计算中的舍入误差，在Win32平台，它被存储为可变长64位整数，其中四个最小有效位隐式地表示小数部分，当在赋值和表达式中与其他real类型混合运算时，货币值会自动除以或乘以10000再参与运算。</p>
<p>  这种类型与<code>OLE</code>和<code>.net</code>实现的<code>currency</code>类型是一致的，与在Win64平台的实现也是相同的（XE 2之后版本），货币类型的这种<code>Int64</code>格式（即<code>value*10000</code>或使用<code>PInt64(@aCurrencyValue)^</code>类型转换）是一种安全、快速的实现模式。</p>
<p>  在mORMot框架中，我们尽量避免在货币运算中使用<code>float</code>类型转换，并实现了一些专用函数（请参阅<a href="">货币处理</a>）以利于快速和安全地通过RTTI存取货币字段，特别是在与JSON文本相互转换时。使用<code>Int64</code>格式不仅可以更快，而且更安全，可避免用<code>float</code>类型转换可能带来的四舍五入问题。对于数据库，特别远程数据库，<code>SynDB.pas</code>单元会尽量避免<code>ftCurrency</code>字段值与<code>double</code>数据类型的相互转换。</p>
<p>  在生产系统中排查四舍五入问题简直就是一场噩梦，拥有一个原生就支持货币类型的底层框架听着就觉得安全。</p>
<h3 id="toc_8">5.1.7. TSQLRecord字段<a class="vnote-anchor" href="#toc_8" data-anchor-icon="#"></a></h3>
<p>  特别说明的是，在通常的Delphi代码中，<code>TSQLRecord</code>字段默认情况下不是类的实例。在执行<code>TSQLRecord.Create()</code>或<code>CreateAndFillPrepare()</code>构造函数后，千万不要直接调用<code>aMyRecord.AnotherRecord.Property</code>，否则将导致非法访问错误。<br>
  实际上，<code>TSQLRecord</code>字段用于定义表之间的”一对多”或“一对一”关系，因此，这个指向另一条记录的字段不是一个真正的类实例，仅标识所指向的<code>TSQLRecord</code>记录的<code>ID</code>。<br>
  这个规则的唯一例外是<code>TSQLRecordMany</code>类型的字段，按照设计，它使用一个“多对多”关系的数据透视表来存取数据并将其实例化，ORM将自动实例化所有<code>TSQLRecordMany</code>字段，并在销毁类实例时回收它们，因此您不需要维护它们的生命周期。<br>
  注：可以使用<code>TSQLRecord.CreateJoin()</code>构造函数一次性实例化并加载所有<code>TSQLRecord</code>字段，销毁类实例时也完成对它们的回收。<br>
  ORM将自动对TSQLRecord字段进行以下优化：</p>
<ul>
<li>
<p>在数据库中为<code>TSQLRecord</code>字段创建索引；</p>
</li>
<li>
<p>当删除一个被引用的记录时，ORM会自动将所有指向该记录的字段值设置为0。</p>
</li>
</ul>
<p>  实际上，ORM没有使用SQL的<code>ON DELETE SET DEFAULT</code>外键约束语法，这个特性不是在RDBMS级实现，而是由ORM实现。<br>
  有关如何使用<code>TSQLRecord</code>字段的详细信息，请参阅<a href="">一对一或一对多</a>。</p>
<h3 id="toc_9">5.1.8. TID字段<a class="vnote-anchor" href="#toc_9" data-anchor-icon="#"></a></h3>
<p>  <code>TSQLRecord</code>引用字段是关联类实例的指针，因此是32位的（至少对Win32/Linux32程序是这样），由于<code>TSQLRecord.ID</code>字段被声明为<code>TID = Int64</code>，如果存储的<code>ID</code>大于2,147,483,647（有符号32位值），可能会丢失信息。</p>
<p>  您可以定义<code>TID</code>字段来存储范围在9,223,372,0366,854,775,808内的任意主键值，需要注意的是，<code>TID</code>没有记录引用的关联表信息。</p>
<p>  ORM会对<code>TID</code>字段进行以下优化：</p>
<ul>
<li>在数据库中为其创建索引；</li>
<li>删除引用记录时，ORM不会做任何事情，因为它没有关联表信息，这是与TSQLRecord字段的主要区别。</li>
</ul>
<p>  您也可以使用定制的<code>TID</code>类型来定义关联表，如使用<code>TableNameID</code>命名形式定义一个<code>TID</code>的子类，譬如：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  TSQLRecordClientID = <span class="hljs-keyword">type</span> TID;
  TSQLRecordClientToBeDeletedID = <span class="hljs-keyword">type</span> TID;

  <span class="hljs-title">TSQLOrder</span> = <span class="hljs-keyword">class</span>(TSQLRecord)
  ...
  <span class="hljs-keyword">property</span> Client: TID
    <span class="hljs-keyword">read</span> fClient <span class="hljs-keyword">write</span> fClient;
  <span class="hljs-keyword">property</span> OrderedBy: TSQLRecordClientID
    <span class="hljs-keyword">read</span> fOrderedBy <span class="hljs-keyword">write</span> fOrderedBy;
  <span class="hljs-keyword">property</span> OrderedByCascade: TSQLRecordClientToBeDeletedID
    <span class="hljs-keyword">read</span> fOrderedByCascade <span class="hljs-keyword">write</span> fOrderedByCascade;
  ...
</code></pre>
<p>  这里定义了三个存储<code>Int64</code>的外键字段，ORM将确保在数据库上创建相应的索引，以加速对其值的搜索。但是<code>TID</code>、<code>TSQLRecordClientID</code>或<code>TSQLRecordClientToBeDeletedID</code>的类型定义了删除记录时不同的处理方式。</p>
<p>  对于通用的<code>TID</code>类型，它定义的第一个<code>Client</code>属性没有任何关联表的引用关系，因此不会出现级联删除。</p>
<p>  另外两个字段，<code>OrderedBy</code>和<code>OrderedByCascade</code>，按照命名约定，是关联到数据模型的<code>TSQLRecordClient</code>表，ORM将根据<code>'TSQLRecordClientID'</code>、<code>'TSQLRecordClientToBeDeletedID'</code>命名，并通过规则表达式<code>*[ToBeDeleted]ID</code>识别到与<code>TSQLRecordClient</code>表的关联关系。</p>
<p>  因此，ORM将跟踪<code>TSQLRecordClient</code>表记录的删除操作，对于引用记录被删除的，它将确保与其关联的<code>OrderedBy</code>字段被置为0，而关联的<code>OrderedByCascade</code>字段的记录将被删除。需说明的是，框架没有使用SQL语句<code>ON DELETE SET DEFAULT</code>或<code>ON DELETE CASCADE</code>外键约束来实现，而是在ORM中实现。</p>
<h3 id="toc_10">5.1.9. TRecordReference和TRecordReferenceToBeDeleted字段<a class="vnote-anchor" href="#toc_10" data-anchor-icon="#"></a></h3>
<p>  <code>TSQLRecord</code>或<code>TID</code>字段是引用单个<code>TSQLRecord</code>关联表，您可以使用<code>TRecordReference</code>或<code>TRecordReferencetobedeleted</code>字段来引用数据模型中的任意表中的任意记录。</p>
<p>  上述字段使用<code>Int64</code>存储了对一个<code>TSQLRecord类</code>（定义了表）和一个<code>ID</code>（定义了行）的引用。</p>
<p>  之后您可以调用<code>TSQLRest.Retrieve(Reference)</code>来获取记录内容。</p>
<p>  切记：对表的引用在<code>TSQLModel</code>模型中是将其索引存储在一个<code>TSQLRecord</code>类中。</p>
<p>  因此，要让<code>TRecordReference*</code>正常工作，您需要确保：</p>
<ul>
<li><code>TSQLRecord</code>类在<code>TSQLModel</code>模型的顺序不能变，包括修改之后都不能变，否则，所有以前存储的<code>TRecordReference*</code>值都可能指向错误的记录；</li>
<li>客户端和服务端共享同一个模型，至少对使用了<code>TRecordReference*</code>的<code>TSQLRecord</code>类来说是这样。</li>
</ul>
<p>  ORM将跟踪被引用记录的删除操作，并根据引用类型进行处理：</p>
<ul>
<li><code>TRecordReference</code>引用字段将被置为0，与使用`ON DELETE SET DEFAULT外键约束的SQL语句结果一样。</li>
<li><code>TRecordReferenceToBeDeleted</code>引用将导致整条记录被删除，与使用`ON DELETE CASCADE外键约束的SQL语句结果一样。</li>
</ul>
<p>  就像<code>TSQLRecord</code>或<code>TSQLRecordClassName[ToBeDeleted]ID</code>字段一样，这种删除跟踪不是在RDBMS中定义的，而是在ORM中模拟完成的。</p>
<p>  为了更便于处理<code>TRecordReference</code>值（实际上就是普通的<code>Int64</code>值），您可以将它们转换为<code>RecordRef()</code>记录，并通过辅助方法来访问其存储信息，参见<a href="">数据库设计</a>中使用<code>TRecordReference</code>引用类型的数据模型例子，如<code>TSQLAuditTrail</code>表的<code>AssociatedRecord</code>字段。</p>
<h3 id="toc_11">5.1.10. TSQLRecord、TID、TRecordReference的级联删除<a class="vnote-anchor" href="#toc_11" data-anchor-icon="#"></a></h3>
<p>  框架所提供的所有外键引用对照表如下：</p>
<table>
<thead>
<tr>
<th>类型定义</th>
<th>创建索引</th>
<th>可关联表</th>
<th>级联删除</th>
<th>模拟的SQL操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>TSQLRecord</td>
<td>是</td>
<td>一个</td>
<td>置为0</td>
<td>ON DELETE SET DEFAULT</td>
</tr>
<tr>
<td>TID</td>
<td>是</td>
<td>无</td>
<td>无</td>
<td>无</td>
</tr>
<tr>
<td>TClassNameID</td>
<td>是</td>
<td>一个</td>
<td>置为0</td>
<td>ON DELETE SET DEFAULT</td>
</tr>
<tr>
<td>TClassNameToBeDeletedID</td>
<td>是</td>
<td>一个</td>
<td>删除行</td>
<td>ON DELETE CASCADE</td>
</tr>
<tr>
<td>TRecordReference</td>
<td>是</td>
<td>所有</td>
<td>置为0</td>
<td>ON DELETE SET DEFAULT</td>
</tr>
<tr>
<td>TRecordReferenceToBeDeleted</td>
<td>是</td>
<td>所有</td>
<td>删除行</td>
<td>ON DELETE CASCADE</td>
</tr>
</tbody>
</table>
<p>  值得指出的是，这种级联删除不是在RDBMS中定义的，而是在ORM中定义的。</p>
<p>  因此，这样就可以适配各种数据库，包括<a href="">NoSQL和对象文件映射（ODM）</a>，事实上，RDBMS数据库引擎不允许在多个表上定义这样的<code>ON DELETE</code>触发器，而mORMot则可使用<code>TRecordReference</code>来处理这样的复杂引用。</p>
<h3 id="toc_12">5.1.11. Variant字段<a class="vnote-anchor" href="#toc_12" data-anchor-icon="#"></a></h3>
<p>  ORM在数据库中会将<code>variant</code>变体字段存储为JSON文本。</p>
<p>  在加载时系统将检查它们的内容：</p>
<ul>
<li>如果使用了自定义变体类型（例如MongoDB的自定义对象），它们将被还原为自定义变体类型（通过其提供的扩展语法）；</li>
<li>如果存储的是JSON文本对象或数组，则创建一个<code>TDocVariant</code>变体类型的实例；</li>
<li>如果存储的文本是数值，则会创建一个整数或双精度类型的数值；</li>
<li>否则，就创建一个字符串。</li>
</ul>
<p>  由于所有数据都以文本的形式存储在列中，因此您的查询可确保任何SQL语句都按照预期的方式处理它（如在比较之前进行数字转换），即使SQLite3能够改变每一行的列类型（形如Delphi代码的变体变量），我们也没有使用这些特性，因为我们希望框架能够与所有数据库一起工作，而SQLite3是唯一具有此特性的。</p>
<p>  在JSON级别，根据存储的值，变体字段可作为JSON文本或数字进行传递。</p>
<p>  如果您使用远程NoSQL的MongoDB数据库，请参阅<a href="">MongoDB+ORM=ODM</a>，这样的变体字段不会以JSON文本的形式存储，而是以真正的BSON文档的形式存储。因此，根据需要，您将能够使用这个数据库引擎的所有高级搜索和索引功能。</p>
<h3 id="toc_13">5.1.12. Record字段<a class="vnote-anchor" href="#toc_13" data-anchor-icon="#"></a></h3>
<p>  从Delphi XE5版本开始，您可以在<code>TSQLRecord</code>中直接定义和使用<code>Record</code>类型的发布属性：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-title">TSQLMyRecord</span> = <span class="hljs-keyword">class</span>(TSQLRecordPeople)
  <span class="hljs-keyword">protected</span>
    fGUID: TGUID;
  <span class="hljs-keyword">published</span>
    <span class="hljs-keyword">property</span> GUID: TGUID <span class="hljs-keyword">read</span> fGUID <span class="hljs-keyword">write</span> fGUID <span class="hljs-keyword">index</span> <span class="hljs-number">38</span>;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  <strong><em>译注：Free Pascal的TGUID（全局唯一标识符）记录定义如下（objpash.inc line 129）：</em></strong></p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span> TGuid = <span class="hljs-keyword">packed</span> <span class="hljs-keyword">record</span>
  <span class="hljs-keyword">case</span> Integer <span class="hljs-keyword">of</span>
    <span class="hljs-number">1</span>: (
        Data1: DWord;					<span class="hljs-comment">//First 4 bytes of GUID</span>
        Data2: Word;					<span class="hljs-comment">//Bytes 5 and 6 of GUID</span>
        Data3: Word;					<span class="hljs-comment">//Bytes 7 and 8 of GUID</span>
        Data4: <span class="hljs-keyword">array</span> [<span class="hljs-number">0</span>..<span class="hljs-number">7</span>] <span class="hljs-keyword">of</span> Byte;	<span class="hljs-comment">//Bytes 9-17 of GUID</span>
      );
    <span class="hljs-number">2</span>: (
        D1: DWord;						<span class="hljs-comment">//First 4 bytes of GUID</span>
        D2: Word;						<span class="hljs-comment">//Bytes 5 and 6 of GUID</span>
        D3: Word;						<span class="hljs-comment">//Bytes 7 and 8 of GUID</span>
        D4: <span class="hljs-keyword">array</span> [<span class="hljs-number">0</span>..<span class="hljs-number">7</span>] <span class="hljs-keyword">of</span> Byte;		<span class="hljs-comment">//Bytes 9-17 of GUID</span>
      );
    <span class="hljs-number">3</span>: (
        time_low: DWord;				<span class="hljs-comment">//low part of GUID timestamp</span>
        time_mid: Word;					<span class="hljs-comment">//Middle part of GUID timestamp</span>
        time_hi_and_version: Word;		<span class="hljs-comment">//High part of GUID timestamp and version</span>
        clock_seq_hi_and_reserved: Byte;<span class="hljs-comment">//High part of clock sequence of GUID</span>
        clock_seq_low: Byte;			<span class="hljs-comment">//Low part of clock sequence of GUID</span>
        node: <span class="hljs-keyword">array</span> [<span class="hljs-number">0</span>..<span class="hljs-number">5</span>] <span class="hljs-keyword">of</span> Byte;		<span class="hljs-comment">//Node part of GUID</span>
      );
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  record将被序列化为JSON，在这里TGUID将被序列化为JSON字符串，然后作为TEXT存储在数据库中。</p>
<p>  我们指定了一个<code>index 38</code>来声明，当存储在外部数据库中时，该列最多将包含38个字符,参见<a href="">代码优先ORM</a>。</p>
<p>  records发布属性由我们的代码处理，因为在Delphi XE5之前，Delphi不会为这些属性创建相应的RTTI。</p>
<p>  因此，上面的类定义的<code>record</code>不能直接用于较旧版本的Delphi或FreePascal。</p>
<p>  您可以使用一个只有一个元素的动态数组，以便在<code>TSQLRecord</code>类定义中处理record，参见<a href="">基于SQL代码的动态数组</a>，但这种方法可能令人困惑。</p>
<p>  如果您想在Delphi XE5之前的版本使用这些属性，您可以重载给定表的<code>TSQLRecord.InternalRegisterCustomProperties()</code>虚方法，以显式地定义一个record。</p>
<p>  例如，注册一个<code>GUID</code>属性对应的<code>TSQLMyRecord.fGUID: TGUID</code>字段：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-title">TSQLMyRecord</span> = <span class="hljs-keyword">class</span>(TSQLRecord)
  <span class="hljs-keyword">protected</span>
    fGUID: TGUID;
    <span class="hljs-keyword">class</span> <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">InternalRegisterCustomProperties</span><span class="hljs-params">(Props: TSQLRecordProperties)</span>;</span> <span class="hljs-keyword">override</span>;
  <span class="hljs-keyword">public</span>
    <span class="hljs-keyword">property</span> GUID: TGUID <span class="hljs-keyword">read</span> fGUID <span class="hljs-keyword">write</span> fGUID;
  <span class="hljs-keyword">end</span>;

<span class="hljs-comment">{ TSQLMyRecord }</span>

<span class="hljs-keyword">class</span> <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TSQLMyRecord</span>.<span class="hljs-title">InternalRegisterCustomProperties</span><span class="hljs-params">(
  Props: TSQLRecordProperties)</span>;</span>
<span class="hljs-keyword">begin</span>
  Props.RegisterCustomPropertyFromTypeName(self,<span class="hljs-string">'TGUID'</span>,<span class="hljs-string">'GUID'</span>,
    @TSQLRecordCustomProps(<span class="hljs-keyword">nil</span>).fGUID,[aIsUnique],<span class="hljs-number">38</span>);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  您也可以调用<code>Props.RegisterCustomPropertyFromRTTI()</code>，提供包含引用计数字段（如字符串、变量或嵌套动态数组）的记录的<code>TypeInfo()</code>指针。当然，支持任何给定记录类型的自定义JSON序列化。</p>
<p>  这些自定义记录注册方法定义了以下两种方法:</p>
<ul>
<li>TEXT序列化，使用<code>RegisterCustomPropertyFromRTTI()</code>或<br>
<code>RegisterCustomPropertyFromTypeName()</code>；</li>
<li>BLOB序列化，使用<code>RegisterCustomRTTIRecordProperty()</code>或<br>
<code>RegisterCustomFixedSizeRecordProperty()</code>。</li>
</ul>
<h3 id="toc_14">5.1.13. BLOB字段<a class="vnote-anchor" href="#toc_14" data-anchor-icon="#"></a></h3>
<p>  实际上，有几种属性将作为BLOB存储在数据库后端：</p>
<ul>
<li><code>TSQLRawBlob</code>属性是存储二进制数据的类型，例如图像或文档；</li>
<li>动态数组（调用<code>TDynArray.SaveTo</code>保存的二进制格式）;</li>
<li>显式注册为<code>BLOB</code>列的记录。</li>
</ul>
<p>  默认情况下，动态数组和BLOB记录内容都将从数据库中检索为Base64编码文本。</p>
<p>  但是<code>TSQLRawBlob</code>属性可作为RESTful资源单独进行传输，这是REST方案所要求的，这意味着第一次请求将检索所有“简单”字段为JSON，然后再通过其它请求将每个BLOB字段检索到二进制缓冲区。因此，默认情况下不传输<code>TSQLRawBlob</code>字段，以节省传输带宽和资源。</p>
<p>  您可以通过如下设置修改其默认值：</p>
<ul>
<li>要么通过<code>TSQLRestClientURI.ForceBlobTransfert: boolean</code>属性，以强制传输数据模型的所有表的所有BLOB字段，<code>SynFiles</code>示例就是这样做的，请参阅本文件稍后部分；</li>
<li>或通过<code>TSQLRestClientURI.TSQLRestClientURI.ForceBlobTransfertTable[]</code>属性，指定模型中的表。</li>
</ul>
<h3 id="toc_15">5.1.14. 允许空值的TNullable*字段<a class="vnote-anchor" href="#toc_15" data-anchor-icon="#"></a></h3>
<p>  在Delphi中，不存在空值类型，在c#中通过<code>int?</code>的方式来定义，但是在SQL和JSON中空值确实存在，并且在ORM中也支持。</p>
<p>  在SQLite3中，NULL是按照http://www.sqlite.org/lang_expr.html中表述的方式进行处理（参见<code>IS</code>和<code>IS NOT</code>操作符）。</p>
<p>  值得注意的是，对NULL的处理在各种数据库引擎中并不一致，如将NULL与非NULL值比较等，因此，当您从一个数据库引擎切换到另一个数据库引擎时，我们建议在任何数据库语句中谨慎使用它，或者只在经过严格测试的单元中使用它。</p>
<p>  默认情况下，在mORMot ORM/SQL代码中，NULL只出现在大小为<code>0</code>字节的BLOB存储中。换句话说，与大多数的类型相比，您不应该将其视为值，请参阅<a href="#5.1.">TSQLRecord字段定义</a>。</p>
<p>  面向空值的类型已经在我们的框架中实现了，因为object pascal语言还不允许定义空值类型，我们选择将这些值存储为变体，并使用一组<code>TNullable*</code>专用类型，它们在<code>mORMot.pas</code>中定义如下：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  TNullableInteger = <span class="hljs-keyword">type</span> variant;
  TNullableBoolean = <span class="hljs-keyword">type</span> variant;
  TNullableFloat = <span class="hljs-keyword">type</span> variant;
  TNullableCurrency = <span class="hljs-keyword">type</span> variant;
  TNullableDateTime = <span class="hljs-keyword">type</span> variant;
  TNullableTimeLog = <span class="hljs-keyword">type</span> variant;
  TNullableUTF8Text = <span class="hljs-keyword">type</span> variant;
</code></pre>
<p>  为了定义此类型的可空列，可以在<code>TSQLRecord</code>类中定义它们：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-title">TSQLNullableRecord</span> = <span class="hljs-keyword">class</span>(TSQLRecord)
<span class="hljs-keyword">protected</span>
  fInt: TNullableInteger;
  fBool: TNullableBoolean;
  fFlt: TNullableFloat;
  fCurr: TNullableCurrency;
  fDate: TNullableDateTime;
  fTimestamp: TNullableTimeLog;
  fCLOB: TNullableUTF8Text;
  fText: TNullableUTF8Text;
<span class="hljs-keyword">published</span>
  <span class="hljs-keyword">property</span> Int: TNullableInteger <span class="hljs-keyword">read</span> fInt <span class="hljs-keyword">write</span> fInt;
  <span class="hljs-keyword">property</span> Bool: TNullableBoolean <span class="hljs-keyword">read</span> fBool <span class="hljs-keyword">write</span> fBool;
  <span class="hljs-keyword">property</span> Flt: TNullableFloat <span class="hljs-keyword">read</span> fFlt <span class="hljs-keyword">write</span> fFlt;
  <span class="hljs-keyword">property</span> Curr: TNullableCurrency <span class="hljs-keyword">read</span> fCurr <span class="hljs-keyword">write</span> fCurr;
  <span class="hljs-keyword">property</span> Date: TNullableDateTime <span class="hljs-keyword">read</span> fDate <span class="hljs-keyword">write</span> fDate;
  <span class="hljs-keyword">property</span> Timestamp: TNullableTimeLog <span class="hljs-keyword">read</span> fTimestamp <span class="hljs-keyword">write</span> fTimestamp;
  <span class="hljs-keyword">property</span> CLOB: TNullableUTF8Text <span class="hljs-keyword">read</span> fCLOB <span class="hljs-keyword">write</span> fCLOB;
  <span class="hljs-keyword">property</span> Text: TNullableUTF8Text <span class="hljs-keyword">index</span> <span class="hljs-number">32</span> <span class="hljs-keyword">read</span> fText <span class="hljs-keyword">write</span> fText;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  这样的类将允许ORM按照需要的方式处理SQL NULL值，即返回一个<code>NULL</code>变体值，或者如果存储有内容时，返回一个整数/数字/文本值，当然，数据库中相应的列将具有对应的数据类型，例如<code>TNullableInteger</code>属性是一个可空整数。</p>
<p>  注意，<code>TNullableUTF8Text</code>通常被定义为<code>RawUTF8</code>字段，请参阅<a href="#5.1.1">property特性</a>。也就是说，默认情况下，没有任何大小限制（与<code>CLOB</code>属性类似），或者使用<code>index ###</code>附加特性（如上述的<code>Text</code>属性定义，它将被转换为<code>VARCHAR(32)</code>类型的 SQL列）。</p>
<p>  您可以使用以下函数从任何不可为空的标准Delphi值中创建一个<code>TNullable*</code>值:</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NullableInteger</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Value: Int64)</span>:</span> TNullableInteger;
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NullableBoolean</span><span class="hljs-params">(Value: boolean)</span>:</span> TNullableBoolean;
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NullableFloat</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Value: double)</span>:</span> TNullableFloat;
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NullableCurrency</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Value: currency)</span>:</span> TNullableCurrency;
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NullableDateTime</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Value: TDateTime)</span>:</span> TNullableDateTime;
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NullableTimeLog</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Value: TTimeLog)</span>:</span> TNullableTimeLog;
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NullableUTF8Text</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Value: RawUTF8)</span>:</span> TNullableUTF8Text;
</code></pre>
<p>  通过强类型转换，常量可转换为对应类型的空值（为兼顾FPC兼容性，不允许直接将<code>null: variant</code>变量赋给声明为<code>TNullable* = type variant</code>的属性）</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span>
  NullableIntegerNull: TNullableInteger <span class="hljs-keyword">absolute</span> NullVarData;
  NullableBooleanNull: TNullableBoolean <span class="hljs-keyword">absolute</span> NullVarData;
...
</code></pre>
<p>  您可以使用以下函数检查<code>TNullable*</code>是否包含<code>null</code>值：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NullableIntegerIsEmptyOrNull</span><span class="hljs-params">(<span class="hljs-keyword">const</span> V: TNullableInteger)</span>:</span> Boolean;
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NullableBooleanIsEmptyOrNull</span><span class="hljs-params">(<span class="hljs-keyword">const</span> V: TNullableBoolean)</span>:</span> Boolean;
...
</code></pre>
<p>  或使用相应的函数获取Delphi中的非空值：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NullableIntegerToValue</span><span class="hljs-params">(<span class="hljs-keyword">const</span> V: TNullableInteger; <span class="hljs-keyword">out</span> Value: Int64)</span>:</span> Boolean;
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NullableBooleanToValue</span><span class="hljs-params">(<span class="hljs-keyword">const</span> V: TNullableBoolean; <span class="hljs-keyword">out</span> Value: Boolean)</span>:</span> Boolean;
...
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NullableIntegerToValue</span><span class="hljs-params">(<span class="hljs-keyword">const</span> V: TNullableInteger)</span>:</span> Int64;
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NullableBooleanToValue</span><span class="hljs-params">(<span class="hljs-keyword">const</span> V: TNullableBoolean; <span class="hljs-keyword">out</span> Value: Boolean)</span>:</span> Boolean;
...
</code></pre>
<p>  在FPC中必须使用上述<code>Nullable*ToValue()</code>函数，FPC不允许混合使用<code>variant</code>变量和<code>TNullable* = type variant</code>变量。</p>
<p>  由于有了这些类型以及它们相应的封装函数，您手边就有了安全地将一些空值存储到应用程序数据库所需的一切东西，使得Delphi能正确处理它们。</p>
<h2 id="toc_16">5.2. 使用对象<a class="vnote-anchor" href="#toc_16" data-anchor-icon="#"></a></h2>
<p>  可以使用下面的CRUD模式的代码来访问特定记录（对记录的创建、检索、更新、删除等操作是通过Add/Update/Delete/Retrieve方法实现），遵循RESTful模式，使用记录的<code>ID</code>主键作为资源标识符：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">Test</span><span class="hljs-params">(Client: TSQLRest)</span>;</span> <span class="hljs-comment">// we will use CRUD operations on a REST instance</span>
<span class="hljs-keyword">var</span> Baby: TSQLBaby; <span class="hljs-comment">// store a record</span>
  ID: TID; <span class="hljs-comment">// store a reference to a record</span>
<span class="hljs-keyword">begin</span>
  <span class="hljs-comment">// create and save a new record, since Smith, Jr was just born</span>
  Baby := TSQLBaby.Create;
  <span class="hljs-keyword">try</span>
    Baby.<span class="hljs-keyword">Name</span> := <span class="hljs-string">'Smith'</span>;
    Baby.Address := <span class="hljs-string">'New York City'</span>;
    Baby.BirthDate := Date;
    Baby.Sex := sMale;
    ID := Client.Add(Baby,true);
  <span class="hljs-keyword">finally</span>
    Baby.Free; <span class="hljs-comment">// manage memory as usual</span>
  <span class="hljs-keyword">end</span>;
  <span class="hljs-comment">// update record data</span>
  Baby := TSQLBaby.Create(Client,ID); <span class="hljs-comment">// retrieve from ID</span>
  <span class="hljs-keyword">try</span>
    assert(Baby.<span class="hljs-keyword">Name</span>=<span class="hljs-string">'Smith'</span>);
    Baby.<span class="hljs-keyword">Name</span> := <span class="hljs-string">'Smeeth'</span>;
    Client.Update(Baby);
  <span class="hljs-keyword">finally</span>
    Baby.Free;
  <span class="hljs-keyword">end</span>;
  <span class="hljs-comment">// retrieve record data</span>
  Baby := TSQLBaby.Create;
  <span class="hljs-keyword">try</span>
    Client.Retrieve(ID,Baby);
    <span class="hljs-comment">// we may have written: Baby := TSQLBaby.Create(Client,ID);</span>
    assert(Baby.<span class="hljs-keyword">Name</span>=<span class="hljs-string">'Smeeth'</span>);
  <span class="hljs-keyword">finally</span>
    Baby.Free;
  <span class="hljs-keyword">end</span>;
  <span class="hljs-comment">// delete the created record</span>
  Client.Delete(TSQLBaby,ID);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  当然，您也可以让<code>TSQLBaby</code>实例拥有更长的生命周期，定义更多的<code>TSQLBaby</code>实例用于访问多条记录，并可根据需要调用<code>Retrieve</code>/<code>Add</code>/<code>Delete</code>/<code>Update</code>等方法。</p>
<p>  如上例，我们无需编写SQL语句，不用关注数据库引擎的差异（例如日期或数字处理），对象使用高级方法访问，包括NoSQL数据库支持，比如<code>TObjectList</code>或MongoDB，这就是ORM的魔力。</p>
<p>  实际上，REST模式并不完全等同于CRUD操作，为了实现ORM的目标，我们必须绑定一些REST动作，完整定义参见<a href="">REST</a>。不过您仅需知道，这些Add/Update/Delete/Retrieve方法能够帮您定义对象的完整生命周期。</p>
<h2 id="toc_17">5.3. 查询<a class="vnote-anchor" href="#toc_17" data-anchor-icon="#"></a></h2>
<h3 id="toc_18">5.3.1. 返回对象列表<a class="vnote-anchor" href="#toc_18" data-anchor-icon="#"></a></h3>
<p>  您可以使用<code>FillPrepare</code>或<code>CreateAndFillPrepare</code>方法查询表中所有名字以'A'开头的男婴：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> aMale: TSQLBaby;
...
aMale := TSQLBaby.CreateAndFillPrepare(Client,
 <span class="hljs-string">'Name LIKE ? AND Sex = ?'</span>,[<span class="hljs-string">'A%'</span>,ord(sMale)]);
<span class="hljs-keyword">try</span>
  <span class="hljs-keyword">while</span> aMale.FillOne <span class="hljs-keyword">do</span>
    DoSomethingWith(aMale);
<span class="hljs-keyword">finally</span>
  aMale.Free;
<span class="hljs-keyword">end</span>;

</code></pre>
<p>  上例循环遍历所有满足条件的记录，并通过<code>TSQLBaby</code>实例逐一访问每一行的内容。</p>
<p>  mORMot引擎将正确创建SELECT查询SQL语句，以JSON的形式获取所有数据，在客户端和服务端之间传输，然后将值转换为<code>TSQLBaby</code>对象实例。在内部，<code>[CreateAnd]FillPrepare</code>/<code>FillOne</code>方法使用记录列表，从服务端获取为JSON，并在内存中逐行解析（使用了<code>TSQLTableJSON</code>内部实例）。</p>
<p>  注意，在所有<code>FillPrepare</code>/<code>CreateAndFillPrepare</code>方法中都有一个可选的<code>aCustomFieldsCSV</code>参数，您可以通过该参数指定要检索的字段名称的CSV列表，通过仅获取部分字段内容的方式，可以节省一些远程带宽，即您可以使用这个<code>aCustomFieldsCSV</code>参数获取需要的数据，而不是全部数据，之后调用更新也可减少数据交互，由于<code>FillPrepare</code>/<code>CreateAndFillPrepare</code>过程清楚获取了哪些数据，因此在更新服务端数据时也仅需更新该部分数据。</p>
<p>  您还可以创建一个<code>TObjectList</code>，或者干脆使用新版本Delphi的<code>TObjectList&lt;T&gt;</code>泛型实例来检索表的所有值：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> aList: TObjectList&lt;TSQLBaby&gt;;
    aMale: TSQLBaby;
...
aList := Client.RetrieveList&lt;TSQLBaby&gt;(
  <span class="hljs-string">'Name LIKE ? AND Sex = ?'</span>,[<span class="hljs-string">'A%'</span>,ord(sMale)]);
<span class="hljs-keyword">try</span>
  <span class="hljs-keyword">for</span> aMale <span class="hljs-keyword">in</span> aList <span class="hljs-keyword">do</span>
     DoSomethingWith(aMale);
<span class="hljs-keyword">finally</span>
  aList.Free;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  请注意，相比前述<code>*FillPrepare</code>调用加<code>while ...FillOne do</code>循环，上面的方法将消耗更多的内存和资源，因为前者仅使用一个<code>TSQLRecord实</code>例，然后直接将返回的JSON内容赋给实例的属性，一次一个。但对于使用庞大的列表的方式，尤其在多线程环境，则可能会出现较大差异。</p>
<p>  当然，采用泛型语法可以使代码更清晰，与业务逻辑集成性更好。</p>
<h3 id="toc_19">5.3.2. 查询参数<a class="vnote-anchor" href="#toc_19" data-anchor-icon="#"></a></h3>
<p>  为使数据库更安全、更快的执行，WHERE子句需要指定一些参数，这些参数将替换<code>[CreateAnd]FillPrepare</code>查询方法中的<code>?</code>并附加到WHERE子句中。</p>
<p>  标准的简单类型的参数（<code>RawUTF8</code>, <code>integer</code>, <code>double</code>, <code>currency</code>..）可以直接替换，就像上面示例代码中<code>Name</code>和<code>Sex</code>属性那样，第一个参数将替换为<code>'A%'</code> <code>RawUTF8</code>文本，第二个参数将替换为整数值<code>1</code>。</p>
<p>  对于指定<code>TDateTime</code>类型的参数值，最好使用<code>DateToSQL()</code>、<code>DateTimeToSQL()</code>或<code>TimeLogToSQL()</code>函数来生成，如下所示：</p>
<pre><code class="lang-pascal hljs">aRec.CreateAndFillPrepare(Client,<span class="hljs-string">'Datum=?'</span>,[DateToSQL(EncodeDate(<span class="hljs-number">2012</span>,<span class="hljs-number">5</span>,<span class="hljs-number">4</span>))]); aRec.CreateAndFillPrepare(Client,<span class="hljs-string">'Datum&gt;=?'</span>,[DateToSQL(<span class="hljs-number">2012</span>,<span class="hljs-number">5</span>,<span class="hljs-number">4</span>)]); aRec.CreateAndFillPrepare(Client,<span class="hljs-string">'Datum&lt;=?'</span>,[DateTimeToSQL(Now)]); aRec.CreateAndFillPrepare(Client,<span class="hljs-string">'Datum&lt;=?'</span>,[TimeLogToSQL(Client.ServerTimestamp)]);
</code></pre>
<p>  对于<code>TTimeLog</code>/<code>TModTime</code>/<code>TCreateTime</code>/<code>TUnixTime</code>这类属性，请优先使用<code>Int64</code>值作为绑定参数。</p>
<p>  如前所述，BLOB（如<code>sftBlob</code>或<code>TSQLRawBlob</code>）属性需调用专用的<code>RetrieveBlob</code>和<code>UpdateBlob</code>方法（或使用全局的<code>RetrieveBlobFields</code>/<code>UpdateBlobFields</code>方法）单独获取。事实上，BLOB数据可能很大（超过几MB）。但对于小的BLOB内容，您可以在准备数据更新时通过重载<code>BinToBase64WithMagic()</code>函数，或者使用<code>TByteDynArray</code>字段来代替<code>TSQLRawBlob</code>字段类型，将其转换成相应的文本格式内容。参见<code>TSQLRestClientURI</code>的<code>ForceBlobTransfert</code>和<code>ForceBlobTransfertTable[]</code>属性。</p>
<p>  注意，<code>TSQLRecord.Create</code>/<code>FillPrepare</code>/<code>CreateAndFillPrepare</code>和<code>TSQLRest.OneFieldValue</code>/<code>MultiFieldValues</code>方法有一个大的变化，在以前的版本，SQL查询的WHERE子句的参数标记为<code>%</code>，并使用<code>:(…):</code>内联；从框架的修订版本1.17，这些方法所需参数标记为<code>?</code>，不再使用<code>:(…):</code>内联，由于这个大的变更，如果您想要从1.16或之前的版本升级，那么需要重新审查一下代码。一般而言，使用<code>?</code>对新用户来说没有什么困惑，更接近于常用的数据库查询方法。<code>TSQLRestClient.ExecuteFmt</code>/<code>ListFmt</code>方法则不受此更改影响，因为它们只是<code>FormatUTF8()</code>函数的封装。</p>
<p>  对于更杂的代码，您可能需要提前准备ORM请求的WHERE子句，您可以使用如下重载<code>FormatUTF8()</code>函数的方法：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> where: RawUTF8;
<span class="hljs-keyword">begin</span>
  where := FormatUTF8(<span class="hljs-string">'id=?'</span>, [], [SomeID]);
  <span class="hljs-keyword">if</span> add_active <span class="hljs-keyword">then</span>
    where := FormatUTF8(<span class="hljs-string">'% and active=?'</span>, [where], [ActiveFlag]);
  <span class="hljs-keyword">if</span> add_date_ini <span class="hljs-keyword">then</span>
    where := FormatUTF8(<span class="hljs-string">'% and date_ini&gt;=?'</span>, [where], [DateToSQL(Date-<span class="hljs-number">2</span>)]);
...
</code></pre>
<p>  这样，基于框架数据库层查询前的语句准备，请求将易于创建和快速执行。</p>
<h3 id="toc_20">5.3.3. TSQLTableJSON介绍<a class="vnote-anchor" href="#toc_20" data-anchor-icon="#"></a></h3>
<p>  如上所述，<code>[CreateAnd]FillPrepare</code>/<code>FillOne</code>方法是调用内部<code>TSQLTableJSON</code>实例实现的。</p>
<p>  简而言之，<code>TSQLTableJSON</code>需要JSON内容作为输入，并将它解析为行和列，与一个或多个可选的<code>TSQLRecord</code>类关联起来，然后让您通过它的<code>Get*</code>方法来访问数据。</p>
<p>  可以如下所示来使用<code>TSQLTableJSON</code>类：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">WriteBabiesStartingWith</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Letters: RawUTF8; Sex: TSex)</span>;</span>
<span class="hljs-keyword">var</span> aList: TSQLTableJSON;
    Row: integer;
<span class="hljs-keyword">begin</span>
  aList := Client.MultiFieldValues(TSQLBaby,<span class="hljs-string">'ID,BirthDate'</span>,
    <span class="hljs-string">'Name LIKE ? AND Sex = ?'</span>,[Letters+<span class="hljs-string">'%'</span>,ord(Sex)]);
  <span class="hljs-keyword">if</span> aList=<span class="hljs-keyword">nil</span> <span class="hljs-keyword">then</span>
    <span class="hljs-keyword">raise</span> Exception.Create(<span class="hljs-string">'Impossible to retrieve data from Server'</span>);
  <span class="hljs-keyword">try</span>
    <span class="hljs-keyword">for</span> Row := <span class="hljs-number">1</span> <span class="hljs-keyword">to</span> aList.RowCount <span class="hljs-keyword">do</span>
      writeln(<span class="hljs-string">'ID='</span>,aList.GetAsInteger(Row,<span class="hljs-number">0</span>),<span class="hljs-string">' BirthDate='</span>,aList.Get(Row,<span class="hljs-number">1</span>));
  <span class="hljs-keyword">finally</span>
    aList.Free;
  <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  对于字段数多的记录，仅检索所需字段可以节省一些带宽。在上面的示例代码中，<code>ID</code>列的字段索引为<code>0</code>（所以通过<code>aList.GetAsInteger(Row,0)</code>检索），生日列的字段索引为<code>1</code>（所以通过<code>aList.Get(Row,1)</code>检索为<code>PUTF8Char</code>）。所有数据行都通过使用<code>RowCount</code>属性的计数循环进行处理，第一行的索引为<code>1</code>，因为第<code>0</code>行将包含列名。</p>
<p>  <code>TSQLTable</code>类有一些直接用于处理游标的方法，如下所示：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">WriteBabiesStartingWith</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Letters: RawUTF8; Sex: TSex)</span>;</span>
<span class="hljs-keyword">var</span> aList: TSQLTableJSON;
<span class="hljs-keyword">begin</span>
  aList := Client.MultiFieldValues(TSQLBaby,<span class="hljs-string">'ID,BirthDate'</span>,
    <span class="hljs-string">'Name LIKE ? AND Sex = ?'</span>,[Letters+<span class="hljs-string">'%'</span>,ord(Sex)]);
  <span class="hljs-keyword">try</span>
    <span class="hljs-keyword">while</span> aList.Step <span class="hljs-keyword">do</span>
      writeln(<span class="hljs-string">'ID='</span>,aList.Field(<span class="hljs-number">0</span>),<span class="hljs-string">' BirthDate='</span>,aList.Field(<span class="hljs-number">1</span>));
  <span class="hljs-keyword">finally</span>
    aList.Free;
  <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  通过使用<code>TSQLTable.Step</code>方法，您不需要检查<code>aList&lt;&gt;nil</code>，因为如果<code>aList</code>未分配到值，它将返回<code>false</code>，您不需要访问<code>RowCount</code>属性，也不需要指定当前行号。</p>
<p>  我们也可以在循环中使用字段名，而不是字段索引：</p>
<pre><code class="lang-pascal hljs">writeln(<span class="hljs-string">'ID='</span>,aList.Field(<span class="hljs-string">'ID'</span>),<span class="hljs-string">' BirthDate='</span>,aList.Field(<span class="hljs-string">'BirthDate'</span>));
</code></pre>
<p>  您还可以使用后期绑定和本地变体来访问字段值，这样的代码可读性更好：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">WriteBabiesStartingWith</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Letters: RawUTF8; Sex: TSex)</span>;</span>
<span class="hljs-keyword">var</span> baby: variant;
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">with</span> Client.MultiFieldValues(TSQLBaby,<span class="hljs-string">'ID,BirthDate'</span>,
    <span class="hljs-string">'Name LIKE ? AND Sex = ?'</span>,[Letters+<span class="hljs-string">'%'</span>,ord(Sex)]) <span class="hljs-keyword">do</span>
  <span class="hljs-keyword">try</span>
    <span class="hljs-keyword">while</span> Step(false,@baby) <span class="hljs-keyword">do</span>
      writeln(<span class="hljs-string">'ID='</span>,baby.ID,<span class="hljs-string">' BirthDate='</span>,baby.BirthDate);
  <span class="hljs-keyword">finally</span>
    Free;
  <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  上面的代码是在运行时搜索绑定的<code>“ID”</code>和<code>“BirthDate”</code>字段，书写成<code>baby.ID</code>和<code>baby.BirthDate</code>的可读性也非常好，<code>with…do</code>语句使代码更短，但应该尽量避免在循环中出现更复杂的处理进程。</p>
<p>  还可参阅<code>TSQLRest</code>的方法：<code>OneFieldValue</code>、 <code>OneFieldValues</code>、 <code>MultiFieldValue</code>、 <code>MultiFieldValues</code>，它们能够按<code>TSQLTableJSON</code>来检索，也能按整数或<code>RawUTF8</code>类型的动态数组来检索。还有<code>TSQLRestClient的List</code>和<code>ListFmt</code>方法，可一次同时连接多个表。</p>
<p>  可以将<code>TSQLTableJSON</code>内容关联到<code>TGrid</code>，利用其关联的<code>TSQLRecord</code>，并根据RTTI方式检索出的列生成用户界面。<code>TSQLTableToGrid</code>类可以将任何<code>TSQLTable</code>关联到标准的<code>TDrawGrid</code>，并提供了一些增强功能：绘制主题、处理Unicode、列类型（如布尔值显示为复选框、日期显示为文本等）、列自动大小、列排序、查找的递增键、可选隐藏列、选择 ……</p>
<h3 id="toc_21">5.3.4. 查询参数相关事项<a class="vnote-anchor" href="#toc_21" data-anchor-icon="#"></a></h3>
<p>  （这段文字作为初学者并不是必须要读的，所以如果你不想了解mORMot的内部结构，你可以跳过它，仅需要记住JSON传输内容中的<code>?</code>将替换为<code>:(…):</code>内联参数，因此可以在任何WHERE子句中直接这样设置）</p>
<p>  再来看看第一个示例代码：</p>
<pre><code class="lang-pascal hljs">aMale := TSQLBaby.CreateAndFillPrepare(Client,
  <span class="hljs-string">'Name LIKE ? AND Sex = ?'</span>,[<span class="hljs-string">'A%'</span>,ord(sMale)]);
</code></pre>
<p>  ORM将根据上述代码将生成一条SQL语句，其中包含SELECT，以及一个WHERE子句，在执行时绑定了两个参数，其中包含<code>'A%'</code>的<code>RawUTF8</code>文本和整数值<code>1</code>。</p>
<p>  实际上，从SQL的角度来看，上述<code>CreateAndFillPrepare()</code>方法调用与下面的代码是一样的：</p>
<pre><code class="lang-pascal hljs">aMale := TSQLBaby.CreateAndFillPrepare(Client,
  <span class="hljs-string">'Name LIKE :(''A%''): AND Sex = :(1):'</span>);
</code></pre>
<p>  或</p>
<pre><code class="lang-pascal hljs">aMale := TSQLBaby.CreateAndFillPrepare(Client,
  <span class="hljs-string">'Name LIKE :(%): AND Sex = :(%):'</span>,[<span class="hljs-string">'''A%'''</span>,ord(sMale)],[]));
</code></pre>
<p>  或</p>
<pre><code class="lang-pascal hljs">aMale := TSQLBaby.CreateAndFillPrepare(Client,
  FormatUTF8(<span class="hljs-string">'Name LIKE :(%): AND Sex = :(%):'</span>,[<span class="hljs-string">'''A%'''</span>,ord(sMale)]));
</code></pre>
<p>  第一点是，正如SQL语法中所预期的那样，字母<code>'A'</code>被加了引号。实际上，<code>Name LIKE :(%): AND Sex = :(%):', ['''A%''',ord(sMale)]</code>确实是SQL查询WHERE子句的有效表达式。</p>
<p>  注意，我们使用了单引号，但我们在<code>:():</code>语句中使用了双引号(")。实际上，SQLite3的原始SQL语句需要单引号，而我们的<code>:():</code>准备语句单引号和双引号都能处理。为了避免混淆，我们总是在文档中显示单引号，但您在<code>:():</code>语句中使用双引号也是安全的，这比在pascal常量字符串中使用双引号更方便。</p>
<p>  上面代码中唯一不清晰的是用于格式化字符串中定义准备参数的<code>:(%):</code>语法。</p>
<p>  实际上，格式化字符串将为WHERE子句生成如下纯文本形式的参数：</p>
<pre><code class="lang-pascal hljs">aMale := TSQLBaby.CreateAndFillPrepare(Client,
  <span class="hljs-string">'Name LIKE :(''A%''): AND Sex = :(1):'</span>);
</code></pre>
<p>  数据库ORM引擎会将其转换成以下SQL查询语句：</p>
<pre><code class="lang-sql hljs"><span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> Baby <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">Name</span> <span class="hljs-keyword">LIKE</span> ? <span class="hljs-keyword">AND</span> Sex = ?;
</code></pre>
<p>  第一个参数将替换为<code>'A%'</code>，第二个参数替换为<code>1</code>。</p>
<p>  实际上，当框架在SQL语句字符串中找到一些<code>:():</code>时，它将准备一个SQL语句，并在执行之前绑定参数（在我们的例子中是字符串<code>A%</code>和整数<code>1</code>），参数的绑定会重用相匹配的SQL语句。有关此机制的详细信息，请参阅<a href="">准备语句</a>。</p>
<p>  如果不使用准备语句，你只能这样：</p>
<pre><code class="lang-pascal hljs">aMale := TSQLBaby.CreateAndFillPrepare(Client,
  <span class="hljs-string">'Name LIKE % AND Sex = %'</span>,[<span class="hljs-string">'''A%'''</span>,ord(sMale)],[]);
</code></pre>
<p>  或</p>
<pre><code class="lang-pascal hljs">aMale := TSQLBaby.CreateAndFillPrepare(Client,
  FormatUTF8(<span class="hljs-string">'Name LIKE % AND Sex = %'</span>,[<span class="hljs-string">'''A%'''</span>,ord(sMale)]));
</code></pre>
<p>  或</p>
<pre><code class="lang-pascal hljs">aMale := TSQLBaby.CreateAndFillPrepare(Client,
  <span class="hljs-string">'Name LIKE ''A%'' AND Sex = 1'</span>);
</code></pre>
<p>  同样会执行以下SQL语句：</p>
<pre><code class="lang-sql hljs"><span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> Baby <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">Name</span> <span class="hljs-keyword">LIKE</span> <span class="hljs-string">'A%'</span> <span class="hljs-keyword">AND</span> Sex = <span class="hljs-number">1</span>;
</code></pre>
<p>  通过使用SQL查询WHERE子句的准备语句，可以对所有以字符<code>'D'</code>开头的女婴使用相同的请求语句：</p>
<pre><code class="lang-pascal hljs">aFemale := TSQLBaby.CreateAndFillPrepare(Client,
  <span class="hljs-string">'Name LIKE :(%): AND Sex = :(%):'</span>, [<span class="hljs-string">'''D%'''</span>,ord(sFemale)]);
</code></pre>
<p>  使用准备语句可以加速数据库引擎，因为SQL查询只需要解析和优化一次。</p>
<p>  第二类查询方法，如下：</p>
<pre><code class="lang-pascal hljs">aList := Client.MultiFieldValues(TSQLBaby,<span class="hljs-string">'ID,BirthDate'</span>,
    <span class="hljs-string">'Name LIKE ? AND Sex = ?'</span>,[Letters+<span class="hljs-string">'%'</span>,ord(Sex)]);
</code></pre>
<p>  与下面的代码相同：</p>
<pre><code class="lang-pascal hljs"> aList := Client.MultiFieldValues(TSQLBaby,<span class="hljs-string">'ID,BirthDate'</span>,
    <span class="hljs-string">'Name LIKE :(%): AND Sex = :(%):'</span>,[QuotedStr(Letters+<span class="hljs-string">'%'</span>),ord(Sex)],[]);
</code></pre>
<p>  或</p>
<pre><code class="lang-pascal hljs">aList := Client.MultiFieldValues(TSQLBaby,<span class="hljs-string">'ID,BirthDate'</span>,
    FormatUTF8(<span class="hljs-string">'Name LIKE :(%): AND Sex = :(%):'</span>,[QuotedStr(Letters+<span class="hljs-string">'%'</span>),ord(Sex)]));
</code></pre>
<p>  在这两种情况下，在语句准备阶段参数都将内联绑定，可以提高执行速度。</p>
<p>  我们使用<code>QuotedStr</code>标准函数来返回带引号的字母参数，以保证生成正确的SQL语句。</p>
<p>  当然，使用<code>?</code>形式的绑定参数比<code>'%'</code>和<code>:(%):</code>以及<code>QuotedStr()</code>函数调用进行参数内联要简单。在您的客户端代码中，您应该更多地使用<code>'?'</code>，但如果您在框架源代码的WHERE子句中发现了<code>':(%):'</code>形式的语法，也不需要感到惊讶。</p>
<h2 id="toc_22">5.4. TSQLRecord内存自动处理<a class="vnote-anchor" href="#toc_22" data-anchor-icon="#"></a></h2>
<p>  对象非常强大，但需要使用<code>try .. finally</code>语句管理对象实例的生命周期。通常，<code>TSQLRecord</code>的生命周期非常短，我们为局部变量分配一个实例，然后在它超出作用域时释放它。</p>
<p>  我们再次对<code>TSQLBaby</code>样例重写如下：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NewMaleBaby</span><span class="hljs-params">(Client: TSQLRest; <span class="hljs-keyword">const</span> <span class="hljs-keyword">Name</span>,Address: RawUTF8)</span>:</span> TID;
<span class="hljs-keyword">var</span> Baby: TSQLBaby;   <span class="hljs-comment">// store a record</span>
<span class="hljs-keyword">begin</span>
  Baby := TSQLBaby.Create;
  <span class="hljs-keyword">try</span>
    Baby.<span class="hljs-keyword">Name</span> := <span class="hljs-keyword">Name</span>;
    Baby.Address := Address;
    Baby.BirthDate := Date;
    Baby.Sex := sMale;
    result := Client.Add(Baby,true);
  <span class="hljs-keyword">finally</span>
    Baby.Free;
  <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  为了简化这种常见的模式，框架在<code>TSQLRecord</code>级提供了一些自动化的内存管理：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NewMaleBaby</span><span class="hljs-params">(Client: TSQLRest; <span class="hljs-keyword">const</span> <span class="hljs-keyword">Name</span>,Address: RawUTF8)</span>:</span> TID;
<span class="hljs-keyword">var</span> Baby: TSQLBaby;   <span class="hljs-comment">// store a record</span>
<span class="hljs-keyword">begin</span>
  TSQLBaby.AutoFree(Baby);  <span class="hljs-comment">// no try..finally needed!</span>
  Baby.<span class="hljs-keyword">Name</span> := <span class="hljs-keyword">Name</span>;
  Baby.Address := Address;
  Baby.BirthDate := Date;
  Baby.Sex := sMale;
  result := Client.Add(Baby,true);
<span class="hljs-keyword">end</span>; <span class="hljs-comment">// local Baby instance will be released here</span>
</code></pre>
<p>  对查询也同样可用，而不仅仅是写：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> aMale: TSQLBaby;
...
  aMale := TSQLBaby.CreateAndFillPrepare(Client,
    <span class="hljs-string">'Name LIKE ? AND Sex = ?'</span>,[<span class="hljs-string">'A%'</span>,ord(sMale)]);
  <span class="hljs-keyword">try</span>
    <span class="hljs-keyword">while</span> aMale.FillOne <span class="hljs-keyword">do</span>
      DoSomethingWith(aMale);
  <span class="hljs-keyword">finally</span>
    aMale.Free;
  <span class="hljs-keyword">end</span>;

</code></pre>
<p>  可以写成：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> aMale: TSQLBaby;
...
  TSQLBaby.AutoFree(aMale,Client,<span class="hljs-string">'Name LIKE ? AND Sex = ?'</span>,[<span class="hljs-string">'A%'</span>,ord(sMale)]);
  <span class="hljs-keyword">while</span> aMale.FillOne <span class="hljs-keyword">do</span>
    DoSomethingWith(aMale);
</code></pre>
<p>  无需再使用<code>try ... finally</code>块。</p>
<p>  请参阅<code>mORMot.pas</code>中的<code>TSQLRecord.AutoFree()</code>的几个重载方法，以及<code>SynCommon.pas</code>中定义的<code>TAutoFree</code>/<code>IAutoFree</code>相关类型。您可以在<code>TSQLRecord.AutoFree()</code>或<code>TAutoFree.Create()</code>调用中一次性初始化多个局部变量。</p>
<p>  框架并没有像c#或Java那样引入某种神奇的垃圾收集器，也不像Apple和下一代Delphi编译器采用了ARC内存模型，它只是创建了一个局部的隐藏的<code>IAutoFree</code>接口，编译器将在方法的结尾释放该接口，并释放所有关联的类实例。因此，局部的类实例应该保持在局部范围内，而不能在另一个进程中使用和存储，否则，您可能会遭遇访问越界。</p>
<p>  由于FPC接口实现的一个问题（特性?），参见http://bugs.freepascal.org/view.php?id=26602 ，上面的代码在FPC中不会工作，您需要将初始化调用的结果赋给IAutoFree局部变量，如下所示：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> aMale: TSQLBaby;
    auto: IAutoFree;
...
  auto := TSQLBaby.AutoFree(aMale,Client,<span class="hljs-string">'Name LIKE ? AND Sex = ?'</span>,[<span class="hljs-string">'A%'</span>,ord(sMale)]);
  <span class="hljs-keyword">while</span> aMale.FillOne <span class="hljs-keyword">do</span>
    DoSomethingWith(aMale);
</code></pre>
<p>  另一种方法是使用<code>with</code>语句，这样就不需要定义局部变量：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> aMale: TSQLBaby;
...
  <span class="hljs-keyword">with</span> TAutoFree.One(aMale,TSQLBaby.CreateAndFillPrepare(Client,
    <span class="hljs-string">'Name LIKE ? AND Sex = ?'</span>,[<span class="hljs-string">'A%'</span>,ord(sMale)])) <span class="hljs-keyword">do</span>
    <span class="hljs-keyword">while</span> aMale.FillOne <span class="hljs-keyword">do</span>
      DoSomethingWith(aMale);
</code></pre>
<p>  或者使用<code>TSQLRecord.AutoFree</code>重载类方法：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> aMale: TSQLBaby;
...
  <span class="hljs-keyword">with</span> TSQLBaby.AutoFree(aMale,Client,<span class="hljs-string">'Name LIKE ? AND Sex = ?'</span>,[<span class="hljs-string">'A%'</span>,ord(sMale)]) <span class="hljs-keyword">do</span>
    <span class="hljs-keyword">while</span> aMale.FillOne <span class="hljs-keyword">do</span>
      DoSomethingWith(aMale);
</code></pre>
<p>  如果您的代码需要实现Delphi和FPC的交叉编译，请考虑这些FPC编译器所需的特性。</p>
<h2 id="toc_23">5.5. 对象关系：基数<a class="vnote-anchor" href="#toc_23" data-anchor-icon="#"></a></h2>
<p>  <strong><em>Cardinality：基数，描述每个参与实体的可能的关系数目。</em></strong></p>
<p>  如果应用程序需要“平面”数据，那么前面的所有代码都可以。但大多数情况下，您需要定义主/从关系，可能要跨越几个层级。在数据建模中，一个数据表相对于另一个数据表的基数是数据库设计的一个关键方面。数据表之间的关系使用基数这一概念定义一个表如何链接到另一个表。</p>
<p>  在关系模型中，表可以具有以下基数，也可以说，具有以下关系：</p>
<ul>
<li>“一对一”</li>
<li>“多对一”（或反过来“一对多”）</li>
<li>“多对多”（亦即“很多”）</li>
</ul>
<p>  我们的mORMot框架支持所有这些基数类型。</p>
<h3 id="toc_24">5.5.1. “一对一”或“一对多”<a class="vnote-anchor" href="#toc_24" data-anchor-icon="#"></a></h3>
<h4 id="toc_25">5.5.1.1. TSQLRecord类型的字段值是ID，不是实例<a class="vnote-anchor" href="#toc_25" data-anchor-icon="#"></a></h4>
<p>  为了处理表之间的“一对一”或“一对多”关系（即传统RDBMS方法中的主/从信息)，可以在对象定义中定义<code>TSQLRecord</code>字段属性。</p>
<p>  例如，可以这样声明类：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-title">TSQLMyFileInfo</span> = <span class="hljs-keyword">class</span>(TSQLRecord)
  <span class="hljs-keyword">private</span>
    FMyFileDate: TDateTime;
    FMyFileSize: Int64;
  <span class="hljs-keyword">published</span>
    <span class="hljs-keyword">property</span> MyFileDate: TDateTime <span class="hljs-keyword">read</span> FMyFileDate <span class="hljs-keyword">write</span> FMyFileDate;
    <span class="hljs-keyword">property</span> MyFileSize: Int64 <span class="hljs-keyword">read</span> FMyFileSize <span class="hljs-keyword">write</span> FMyFileSize;
  <span class="hljs-keyword">end</span>;

  <span class="hljs-title">TSQLMyFile</span> = <span class="hljs-keyword">class</span>(TSQLRecord)
  <span class="hljs-keyword">private</span>
    FSecondOne: TSQLMyFileInfo;
    FFirstOne: TSQLMyFileInfo;
    FMyFileName: RawUTF8;
  <span class="hljs-keyword">published</span>
    <span class="hljs-keyword">property</span> MyFileName: RawUTF8 <span class="hljs-keyword">read</span> FMyFileName <span class="hljs-keyword">write</span> FMyFileName;
    <span class="hljs-keyword">property</span> FirstOne: TSQLMyFileInfo <span class="hljs-keyword">read</span> FFirstOne <span class="hljs-keyword">write</span> FFirstOne;
    <span class="hljs-keyword">property</span> SecondOne: TSQLMyFileInfo <span class="hljs-keyword">read</span> FSecondOne <span class="hljs-keyword">write</span> FSecondOne;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  正如<a href="">TSQLRecord字段定义</a>所述，<code>TSQLRecord</code>字段属性包含的不是<code>TSQLRecord</code>类实例，<br>
而是<code>pointer(RowID)</code>，并作为整数存储在数据库中。</p>
<p>  因此，主要的原则是永远不要直接使用这些字段属性，应将它们视为类实例的普通属性，否则，您将会得到意外的访问越界错误。</p>
<h4 id="toc_26">5.5.1.2 ID转换<a class="vnote-anchor" href="#toc_26" data-anchor-icon="#"></a></h4>
<p>  在创建带<code>TSQLRecord</code>类型的记录时，可为每个子对象使用临时实例，例如：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> One, Two: TSQLMyFileInfo;
     MyFile: TSQLMyFile;
<span class="hljs-keyword">begin</span>
  One := TSQLMyFileInfo.Create;
  Two := TSQLMyFileInfo.Create;
  MyFile := TSQLMyFile.Create;
  <span class="hljs-keyword">try</span>
    One.MyFileDate := ....
    One.MyFileSize := ...
    MyFile.FirstOne := TSQLMyFileInfo(MyDataBase.Add(One,True)); <span class="hljs-comment">// add One and store ID in MyFile.FirstOne</span>
    Two.MyFileDate := ....
    Two.MyFileSize := ...
    MyFile.SecondOne:= TSQLMyFileInfo(MyDataBase.Add(Two,True)); <span class="hljs-comment">// add Two and store ID in MyFile.SecondOne</span>
    MyDataBase.Add(MyFile,true);
  <span class="hljs-keyword">finally</span>
     MyFile.Free;
     Two.Free;
     One.Free;
  <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  注意如下两个语法是一样的：</p>
<pre><code class="lang-pascal hljs">MyFile.FirstOne := TSQLMyFileInfo(MyDataBase.Add(One,True));
MyFile.FirstOne := pointer(MyDataBase.Add(One,True));
</code></pre>
<p>  你也可以先添加<code>One</code>记录：</p>
<pre><code class="lang-pascal hljs">MyDatabase.Add(One,true);
</code></pre>
<p>  然后选用一个如下的表达式将它分配到MyFile记录中：</p>
<pre><code class="lang-pascal hljs">MyFile.FirstOne := TSQLMyFileInfo(One.ID);
MyFile.FirstOne := pointer(One.ID);
MyFile.FirstOne := One.AsTSQLRecord;
</code></pre>
<p>  前两句使用<code>class</code>/<code>pointer</code>类型转换的语句只能在32位环境中工作（因为ID是整数)，使用<code>TSQLRecord.AsTSQLRecord</code>属性则支持所有平台（包括64位平台），并且这种代码可能更易于处理。</p>
<p>  在访问子对象时，不能直接访问<code>FirstOne</code>或<code>SecondOne</code>属性(它们不是类实例，只有整数ID)，而应使用<code>TSQLRecord.Create(aClient: TSQLRest; aPublishedRecord: TSQLRecord: ForUpdate: boolean=false)</code>重载构造函数，如下所示：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> One: TSQLMyFileInfo;
    MyFile: TSQLMyFile;
<span class="hljs-keyword">begin</span>
  MyFile := TSQLMyFile.Create(Client,aMyFileID);
  <span class="hljs-keyword">try</span>
    <span class="hljs-comment">// here MyFile.FirstOne.MyFileDate will trigger an access violation</span>
    One := TSQLMyFileInfo.Create(Client,MyFile.FirstOne);
    <span class="hljs-keyword">try</span>
      <span class="hljs-comment">// here you can access One.MyFileDate or One.MyFileSize</span>
    <span class="hljs-keyword">finally</span>
      One.Free;
    <span class="hljs-keyword">end</span>;
  <span class="hljs-keyword">finally</span>
    MyFile.Free;
  <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  或者使用<code>with</code>语句：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> MyFile: TSQLMyFile;
<span class="hljs-keyword">begin</span>
  MyFile := TSQLMyFile.Create(Client,aMyFileID);
  <span class="hljs-keyword">try</span>
    <span class="hljs-comment">// here MyFile.FirstOne.MyFileDate will trigger an access violation</span>
    <span class="hljs-keyword">with</span> TSQLMyFileInfo.Create(Client,MyFile.FirstOne) <span class="hljs-keyword">do</span>
    <span class="hljs-keyword">try</span>
      <span class="hljs-comment">// here you can access MyFileDate or MyFileSize</span>
    <span class="hljs-keyword">finally</span>
      Free;
    <span class="hljs-keyword">end</span>;
  <span class="hljs-keyword">finally</span>
    MyFile.Free;
  <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  首先，<code>TSQLRecord</code>类型字段映射为整数<code>ID</code>是学习的一个难点，这是我们在类定义中实现“一对一”或“一对多”关系的唯一方法，技术实现上没有采用Delphi编译器的特有特性，这样做的主要缺点是编译器无法在编译时识别一些运行时可能出现的GPF问题，这取决于开发人员编写<code>TSQLRecord</code>属性代码时的正确性，使用<code>AsTSQLRecord</code>属性和重载<code>TSQLRecord.Create(aPublishedRecord)</code>构造函数的方式会更有帮助。</p>
<h4 id="toc_27">5.5.1.3. 实例自动化和联合查询<a class="vnote-anchor" href="#toc_27" data-anchor-icon="#"></a></h4>
<p>  必须手工处理所有嵌套的<code>TSQLRecord</code>实例，这很烦人，也很容易出错。</p>
<p>  另一个可选的方法是，如果您想检索整个<code>TSQLRecord</code>实例，包括其嵌套的<code>TSQLRecord</code>类型字段属性，您可以使用以下两个构造函数中的一个：</p>
<ul>
<li><code>TSQLRecord.CreateJoined(aClient,aID)</code>；</li>
<li><code>TSQLRecord.CreateAndFillPrepareJoined()</code>，后跟一个<code>while FillOne do ....</code> 循环。</li>
</ul>
<p>  这两个构造函数：</p>
<ul>
<li>将自动实例化所有<code>TSQLRecord</code>类型的字段属性；</li>
<li>ORM核心将使用<code>SELECT .... LEFT JOIN ...</code>语句检索所有的属性，包括嵌套的<code>TSQLRecord</code>字段。</li>
<li>然后，在销毁主实例时也同时释放嵌套的<code>TSQLRecord</code>实例（以避免任何意外的内存泄漏）。</li>
</ul>
<p>  所以你可以放心地这样编写：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> MyFile: TSQLMyFile;
<span class="hljs-keyword">begin</span>
  MyFile := TSQLMyFile.CreateJoined(Client,aMyFileID);
  <span class="hljs-keyword">try</span>
    <span class="hljs-comment">// here MyFile.FirstOne and MyFile.SecondOne are true instances</span>
    <span class="hljs-comment">// and have already retrieved from the database by the constructor</span>
    <span class="hljs-comment">// so you can safely access MyFile.FirstOne.MyFileDate or MyFile.SecondOne.MyFileSize here!</span>
  <span class="hljs-keyword">finally</span>
    MyFile.Free; <span class="hljs-comment">// will release also MyFile.FirstOne and MyFile.SecondOne</span>
  <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  需要注意，在从数据库检索这些数据时，它能达到预期目地，但是，在ORM的当前实现中，任何<code>Update()</code>调用都只处理主<code>TSQLRecord</code>主表属性，和嵌套的<code>TSQLRecord</code>字段ID，而不管理嵌套对象属性值。例如，在上面的代码中，<code>aClient.Update(MyFile)</code>将更新<code>TSQLMyFile</code>表，但不会对<code>MyFile.FirstOne</code> 或 <code>MyFile.SecondOne</code> 的表字段做任何修改。这个限制将来可能会被移除，以允许您显式地请求这个特性。</p>
<h3 id="toc_28">5.5.2. "Has many"和"has many through"<a class="vnote-anchor" href="#toc_28" data-anchor-icon="#"></a></h3>
<p>  正如<a href="https://en.wikipedia.org/wiki/Many-to-many_(data_model)">https://en.wikipedia.org/wiki/Many-to-many_(data_model)</a>中写道：</p>
<p>  <em>在系统分析中，多对多关系是一种基数类型，它指的是两个实体（参见实体关系模型）a和B之间的关系，其中a可能包含父行，其中B中有很多子行，反之亦然。例如，把A看作作者，把B看作书籍。一个作者可以写几本书，一本书可以由几个作者写。因为大多数数据库管理系统只支持一对多关系，所以有必要通过第三和第四链接表(例如AB对应两个一对多关系，A-&gt;AB和B-&gt;AB)实现这种关系，在这种情况下，AB的逻辑主键是由两个外键(即A和B的主键副本)组成的。</em></p>
<p>  从记录的角度来看，为了遵循ORM词汇表(从Ruby on Rails、Python或其他ActiveRecord复制过来)，我们可以说“has many”关系。在传统的RDBMS实现中，创建了一个数据透视表，其中包含对两个相关记录的引用，其他一些相关信息也可以存储在这个数据透视表中，如可以使用它存储关联时间或关系对应的权限，这就是所谓的“has many through”关系。</p>
<p>  实际上，在实现“多对多”基数关系时，ORM设计有几种选择：</p>
<ul>
<li>从ORM中将集合映射到关联查询中（如数据透视表是框架从对象列表或集合中抽象出来的，实现“has many”关系，但是必须定义延迟加载，而没有“has many through”关系）；</li>
<li>显式地将透视表处理为ORM类，并提供访问它们的方法（它将允许"has many"和"has many through"关系）。</li>
<li>将集合存储在ORM类属性中（数据分片）。</li>
</ul>
<p>  在mORMot框架中，我们没有实现第一种模式，而是实现了第二个和第三个：</p>
<ul>
<li>您可以使用专用的<code>TSQLRecordMany</code>类映射DB，这允许某些真正的数据透视表可用(即第二种模式)，引入真正的"has many through" 基数；</li>
<li>但是对于大多数应用程序来说，在一个<code>TSQLRecord</code>类中使用<code>TCollection</code> （<code>TPersistent</code>类）或动态数组以及数据分区存储（即第三种模式）听起来确实更容易。</li>
</ul>
<p>  到目前为止，ORM中没有明确的延迟加载特性，自身没有对<code>TSQLRecord</code>集合或列表的处理机制（因为它们确实出现在ORMs的第一种模式中），这听起来像是一种限制，但它允许在您的代码中精确地管理从服务器检索到的数据，并尽可能低地维护带宽和内存使用，使用数据透视表（通过<code>TSQLRecordMany</code>类型的记录）允许对数据进行访问优化，并实现了最优的延迟加载特性。注意，ORM只会为<code>TSQLRecordMany</code>属性字段自动创建实例。</p>
<h4 id="toc_29">5.5.2.1. 无共享体系架构 (或切片)<a class="vnote-anchor" href="#toc_29" data-anchor-icon="#"></a></h4>
<p>  <em>译注：shared nothing architecture，无共享体系架构，一种分布式计算架构，这种架构中不存在集中存储的状态，整个系统中没有资源竞争，这种架构具有非常强的扩张性，在web应用中广泛使用。</em></p>
<h5 id="toc_30">5.5.2.1.1. 在记录中嵌入所有需要的数据<a class="vnote-anchor" href="#toc_30" data-anchor-icon="#"></a></h5>
<p>  定义数据透视表是关系数据库的一种经典而强大的使用方法，可以释放它的强大功能(（特别是当链接数据量很大时）。</p>
<p>  但是，要正确地处理它既不容易也不自然，因为它将一些依赖关系从DB层引入到业务模型中。例如，它引入了一些额外的需求，比如约束/完整性检查和表/类相互依赖。</p>
<p>  此外，在现实生活中，我们没有设置单独的存储空间，而是将所有的细节存储在主数据中。因此，对于实际对象的领域驱动设计来说，这样的数据透视表打破了业务逻辑，再依托今天计算机的计算能力，我们可以安全地将数据集中存储到数据仓库中。</p>
<p>  让我们引用维基百科在http://en.wikipedia.org/wiki/Shared_nothing_architecture上的声明：</p>
<p>  无共享体系架构（SN）是一种分布式计算体系结构，其中每个节点都是独立和自给自足的，并且在整个系统中不存在节点间的资源竞争性，人们通常将SN与大量集中存储信息的系统进行对比，比如数据库、应用服务器或其他类似的单点竞争系统。</p>
<p>  正如我们在<code>TSQLRecord</code>字段定义中所述，在我们的ORM框架中，动态数组或<code>TPersistent</code>/<code>TCollection</code>等高级类型以BLOB或文本的形式存储在主表中，没有外部链接表，没有要维护的主/从信息。实际上，每个<code>TSQLRecord</code>实例内容都可以包含在自身的ORM中。</p>
<p>  特别是，您可以考虑使用存储在变体字段中的自定义<code>TDocVariant</code>变体类型。它将允许存储任何复杂的文档，嵌套对象或对象，它们将以JSON的形式存储和传输。</p>
<p>  当服务端接入的客户端越来越多时，这种分布式数据架构会体现为重要的优势。事实上，所谓的数据分片，或水平分区，是一个经过验证的解决方案，适用于网络级数据库，比如那些社交网站使用的数据库，EBay或Facebook如何在用户量很多的情况下扩大规模？仅仅通过数据库分片就能解决。</p>
<p>  因此，我们可以使用ORM实现一个简单但非常有效的分片机制。内存数据库，或者说SQLite3，是光速数据处理的理想选择。如果使用得当的话，即便是SQLite在大多数情况下都可以很好地进行扩展，参见<a href="">数据基准测试</a>。</p>
<p>  将详细复杂的数据存储在BLOB或JSON格式的文本中可能首先听起来是错误的想法，它确实打破了RDBMS体系结构的一个被广泛接受的原则，但即使是谷歌也不得不打破这个教条，当MySQL或其它类似的广泛使用的数据库试图实现分片时，它需要大量的工作，其他的，比如NoSQL MongoDB，是更好的选择，它们不严格遵守SQL/RDBMS扁平化方案。</p>
<p>  最后一点，这种实现模式更适合领域驱动设计。</p>
<p>  因此，仔细考虑一下，拥有一个无共享体系架构可能拥有的巨大优势，我们的ORM已经准备好打破基于表结构的SQL，让我们更进一步。</p>
<h5 id="toc_31">5.5.2.1.2. 嵌套对象和数组<a class="vnote-anchor" href="#toc_31" data-anchor-icon="#"></a></h5>
<p>  我们刚才描述的“has many”和“has many through”的关系确实遵循关系数据库中使用数据透视表的行关联的典型过程，如果你有一些DB背景知识，这是有意义的，但有时又不值得。</p>
<p>  这种方法的一个缺点是数据被分割成几个表，您应该小心地注意数据的完整性，例如，当您删除一条记录时，对它的所有引用也会在相关的表中被删除。我们的ORM引擎会处理它，但有时可能会失败，特别是如果您直接使用SQL操作表，而不是使用诸如<code>FillMany*</code>或<code>DestGetJoined</code>这样的高级方法的话。</p>
<p>  另一个潜在的问题是，一个业务逻辑单元被分成几个表，因此被分成几个不同的<code>TSQLRecord</code>和<code>TSQLRecordMany</code>类，从ORM的角度来看，这可能会令人困惑。</p>
<p>  从框架修订版本1.13开始，可以将动态数组、<code>TStrings</code>和<code>TCollection</code>用作TSQLRecord类定义中的字段，这还不足以实现所有可能的"Has many" 体系结构，但是在大多数情况下，当您需要在特定记录中添加记录列表时，以及当这个列表不需要作为独立表引用时，都可以使用它。</p>
<p>  动态数组将作为BLOB字段存储在数据库中，使用JSON传输流中的Base64编码检索，然后使用<code>TDynArray</code>封装器进行序列化。因此，只有Delphi客户端能够使用这个字段内容，您将释放ORM的AJAX能力，从而更好地与object pascal代码集成。一些专用的SQL函数已经添加到SQLite引擎中，比如<code>IntegerDynArrayContains</code>，用于从任何查询的WHERE子句中搜索这个BLOB字段内容，这些功能可以通过AJAX查询获得。</p>
<p>  <code>TPersistent</code>/<code>TStrings</code>和<code>TCollection</code>/<code>TObjectList</code>将作为文本字段存储在数据库中，遵循<code>ObjectToJSON</code>函数格式，您甚至可以通过调用<code>TJSONSerializer.RegisterCustomSerializer</code>类方法序列化任何<code>TObject</code>类，或<code>TObjectList</code>实例列表，如果它们之前是由<code>TJSONSerializer. RegisterClassForJSON</code>注册的。这种格式只包含有效的JSON数组或对象，因此可以通过AJAX应用程序对其进行反序列化。</p>
<p>  关于这个主题，为什么/什么时候你应该使用普通的Delphi对象或数组而不是传统的主/从数据库关系，请阅读下面的“对象，而不是表”和“ORM不是DB”段落。</p>
<h6 id="toc_32">5.5.2.1.2.1. TDocVariant和variant字段<a class="vnote-anchor" href="#toc_32" data-anchor-icon="#"></a></h6>
<p>5.5.2.1.2.1.1. 通过变体实现无模式存储</p>
<p>  正如刚才所述，<code>TSQLRecord</code>中数据分片的首选对象是我们的<code>TDocVariant</code>自定义变体类型。</p>
<p>  你可以定义：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-title">TSQLRecordData</span> = <span class="hljs-keyword">class</span>(TSQLRecord)
<span class="hljs-keyword">private</span>
  fName: RawUTF8;
  fData: variant;
<span class="hljs-keyword">public</span>
<span class="hljs-keyword">published</span>
  <span class="hljs-keyword">property</span> <span class="hljs-keyword">Name</span>: RawUTF8 <span class="hljs-keyword">read</span> fTest <span class="hljs-keyword">write</span> fTest <span class="hljs-keyword">stored</span> AS_UNIQUE;
  <span class="hljs-keyword">property</span> Data: variant <span class="hljs-keyword">read</span> fData <span class="hljs-keyword">write</span> fData;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  这里，我们定义了两个索引键，用于访问所有数据记录：</p>
<ul>
<li>通过<code>TSQLRecord</code>类定义的<code>ID: TID</code>属性，将生成SQLite3 <code>RowID</code>主键；</li>
<li>对于<code>Name: RawUTF8</code>属性，将根据<code>“stored AS_UNIQUE”</code>属性标记生成索引。</li>
</ul>
<p>  任何类型的数据都可以存储在<code>Data: variant</code>字段中，在数据库中，它将存储为JSON UTF-8文本，可以从任何客户端（包括AJAX/HTML5应用程序）进行检索，Delphi客户端或服务端可通过其<code>TDocVariant</code>实例在后期绑定访问这些数据。</p>
<p>  通过<code>variant</code>我们重现了NoSQL数据库引擎的无模式方法！由于mORMot的这种设计，您的应用程序能够存储任何类型的文档，并通过HTTP轻松访问它们。</p>
<p>  存储在这样一个数据库中的文档可以有不同的字段集，每个字段有不同的类型。假设我们的数据集合中有以下<code>Data: variant</code>行：</p>
<pre><code class="lang-json hljs">{ name : <span class="hljs-string">"Joe"</span>, x : <span class="hljs-number">3.3</span>, y : [<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">3</span>] }
{ name : <span class="hljs-string">"Kate"</span>, x : <span class="hljs-string">"abc"</span> }
{ q : <span class="hljs-number">456</span> }
</code></pre>
<p>  当使用数据库解决实际问题时，数据结构通常应该保持一致性。但下面这样的情况会更常见，例如，对于持久化学生对象的表：</p>
<pre><code class="lang-json hljs">{ name : <span class="hljs-string">"Joe"</span>, age : <span class="hljs-number">30</span>, interests : <span class="hljs-string">"football"</span> }
{ name : <span class="hljs-string">"Kate"</span>, age : <span class="hljs-number">25</span> }
</code></pre>
<p>  通常，在这种无模式风格和动态类型语言类似。上面这些构造在PHP、Python和Ruby中很容易表示。而且，由于<code>TDocVariant</code>后期绑定的魔力，即使是Delphi也能够在代码中很好地处理这些结构。我们在这里要做的是使数据库的映射变得自然，比如：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> aRec: TSQLRecordData;
    aID: TID;
<span class="hljs-keyword">begin</span>
  <span class="hljs-comment">// initialization of one record</span>
  aRec := TSQLRecordData.Create;
  aRec.<span class="hljs-keyword">Name</span> := <span class="hljs-string">'Joe'</span>;                              <span class="hljs-comment">// one unique key</span>
  aRec.data := _JSONFast(<span class="hljs-string">'{name:"Joe",age:30}'</span>);   <span class="hljs-comment">// create a TDocVariant</span>
  <span class="hljs-comment">// or we can use this overloaded constructor for simple fields</span>
  aRec := TSQLRecordData.Create([<span class="hljs-string">'Joe'</span>,_ObjFast([<span class="hljs-string">'name'</span>,<span class="hljs-string">'Joe'</span>,<span class="hljs-string">'age'</span>,<span class="hljs-number">30</span>])]);
  <span class="hljs-comment">// now we can play with the data, e.g. via late-binding:</span>
  writeln(aRec.<span class="hljs-keyword">Name</span>);     <span class="hljs-comment">// will write 'Joe'</span>
  writeln(aRec.Data);     <span class="hljs-comment">// will write '{"name":"Joe","age":30}' (auto-converted to JSON string)</span>
  aRec.Data.age := aRec.Data.age+<span class="hljs-number">1</span>;    <span class="hljs-comment">// one year older</span>
  aRec.Data.interests := <span class="hljs-string">'football'</span>;   <span class="hljs-comment">// add a property to the schema</span>
  aID := aClient.Add(aRec,true);       <span class="hljs-comment">// will store {"name":"Joe","age":31,"interests":"footbal"}</span>
  aRec.Free;
  <span class="hljs-comment">// now we can retrieve the data either via the aID created integer, or via Name='Joe'</span>
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  这些动态对象的最大好处之一是模式迁移变得非常容易。使用传统RDBMS，代码的发布可能包含数据迁移脚本。此外，每个版本都应该有一个回退迁移脚本，以备回滚。<code>ALTER TABLE</code>操作非常慢，可能导致停机。</p>
<p>  使用无模式的数据组织，90%的数据库调整变得更简单更自动。例如，如果我们希望将GPA添加到student对象中，我们添加属性，重新保存，一切都很好，如果我们查找一个现有的student并引用GPA，就会得到null。此外，如果我们回滚代码，如果我们的代码写得很好，那么现有对象中的新的GPA字段也不会造成问题。</p>
<p>  实际上，SQlite3的索引B-TREE存储非常高效，因此这种结构可以作为更沉重的NoSQL引擎（如MongoDB或CouchDB）的可靠替代。</p>
<p>  有了添加这种“常规”字段的能力，例如添加一些普通的数字（比如预先计算的聚合值）或文本（比如摘要或描述字段），您就可以继续使用SQL进行快速查询，而不需要像NoSQL范例所使用的map/reduce算法那样复杂。您甚至可以使用FTS3/FTS4/FTS5全文搜索；或者使用SQLite3的RTREE高级扩展特性来进行查询。之后，由于mORMot能够访问任何外部数据库引擎，您就可以使用一些存储在Oracle、PostgreSQL或MS SQL企业数据库中的数据，对您的无模式数据执行联合查询。或者稍后切换到一个真正的MongoDB数据库，只需一行代码，见下面。</p>
<p>5.5.2.1.2.1.2 SQL代码的JSON操作</p>
<p>  如前所述，任何<code>variant</code>字段都将被序列化为JSON，然后作为纯文本存储在数据库中。为了对存储的JSON进行复杂查询，可以在最终用户代码中检索它，然后使用相应的<code>TDocVariant</code>实例对其内容进行搜索。当然，所有这些都有显著的性能代价，尤其是当数据快速增长时。</p>
<p>  解决这些性能问题的方法通常是添加一些“常规”的RDBMS索引字段，然后在这些字段上执行请求。但有时，您可能需要对存储的JSON数据进行一些额外的查询，有时可能还需要结合“常规”字段进行查找。</p>
<p>  为了避免慢速的ORM客户端转换，我们定义了一些SQL函数，用于处理JSON。</p>
<table>
<thead>
<tr>
<th>JsonGet(ArrColumn,0)</th>
<th>从JSON数组按索引返回属性值</th>
</tr>
</thead>
<tbody>
<tr>
<td>JsonGet(ObjColumn,'PropName')</td>
<td>从JSON对象中按名称返回属性值</td>
</tr>
<tr>
<td>JsonGet(ObjColumn,'Obj1.Obj2.Prop')</td>
<td>通过路径返回属性值，包括嵌套的JSON对象</td>
</tr>
<tr>
<td>JsonGet(ObjColumn,'Prop1,Prop2')</td>
<td>从JSON对象中按名称列表提取属性</td>
</tr>
<tr>
<td>JsonGet(ObjColumn,'Prop1,Obj1.Prop')</td>
<td>从JSON对象中按名称列表提取属性(包括嵌套的JSON对象)</td>
</tr>
<tr>
<td>JsonGet(ObjColumn,'Prop*')</td>
<td>从JSON对象中通过通配符名称提取属性</td>
</tr>
<tr>
<td>JsonGet(ObjColumn,'Prop*,Obj1.P*')</td>
<td>通过通配符名称(包括嵌套的JSON对象)从JSON对象中提取属性</td>
</tr>
</tbody>
</table>
<p>  如果没有匹配的值，函数将返回SQL<code>NULL</code>。如果匹配的值是简单的JSON文本或数字，它将返回文本、整数或双精度值，作为结果列或传递给WHERE子句。如果返回的值是一个嵌套的JSON对象或数组，它将作为文本返回，序列化为JSON；因此，您可以将其用作另一个<code>JsonGet()</code>函数的输入，甚至可以通过<code>CONCAT()</code>聚合函数连接字符串。</p>
<p>  参数中允许使用逗号分隔的语法（如<code>'Prop1,Prop2,Prop3'</code>）会同时在一个对象中搜索多个属性，并返回一个JSON对象，其中包含所有匹配的值，如<code>'{"Prop2":"Value2"，"Prop3":123}'</code>，如果未检索到<code>Prop1</code>，则不会出现在JSON对象中。</p>
<p>  如果属性名以<code>*</code>结束，它将返回一个JSON对象，其中包含所有匹配的属性。任何嵌套对象的属性名在返回的JSON对象中都将被扩展为{<code>"Obj1.Prop":...}</code>。</p>
<p>  注意，逗号分隔的语法也允许这样的通配符搜索，如下：</p>
<pre><code class="lang-pascal hljs">JsonGet(ObjColumn,<span class="hljs-string">'owner'</span>) = <span class="hljs-comment">{"login":"smith","id":123456}</span> <span class="hljs-keyword">as</span> TEXT
JsonGet(ObjColumn,<span class="hljs-string">'owner.login'</span>) = "smith" <span class="hljs-keyword">as</span> TEXT
JsonGet(ObjColumn,<span class="hljs-string">'owner.id'</span>) = <span class="hljs-number">123456</span> <span class="hljs-keyword">as</span> INTEGER
JsonGet(ObjColumn,<span class="hljs-string">'owner.name'</span>) = NULL
JsonGet(ObjColumn,<span class="hljs-string">'owner.login,owner.id'</span>) = <span class="hljs-comment">{"owner.login":"smith","owner.id":123456}</span> <span class="hljs-keyword">as</span> TEXT
JsonGet(ObjColumn,<span class="hljs-string">'owner.I*'</span>) = <span class="hljs-comment">{"owner.id:123456}</span> <span class="hljs-keyword">as</span> TEXT
JsonGet(ObjColumn,<span class="hljs-string">'owner.*'</span>) = <span class="hljs-comment">{"owner.login":"smith","owner.id":123456}</span> <span class="hljs-keyword">as</span> TEXT
JsonGet(ObjColumn,<span class="hljs-string">'unknown.*'</span>) = NULL
</code></pre>
<p>  另一个名为<code>JsonHas()</code>的函数与<code>JsonGet()</code>类似，将根据提供的属性（由名称或索引指定）是否存在返回TRUE或FALSE。使用<code>JsonHas()</code>可能比使用<code>JsonGet()</code>更快，例如在WHERE子句中，您不想具体处理此属性值时，只想返回包含所需信息的数据行。</p>
<pre><code class="lang-pascal hljs">JsonHas(ObjColumn,<span class="hljs-string">'owner'</span>) = true
JsonHas(ObjColumn,<span class="hljs-string">'owner.login'</span>) = true
JsonHas(ObjColumn,<span class="hljs-string">'owner.name'</span>) = false
JsonHas(ObjColumn,<span class="hljs-string">'owner.i*'</span>) = true
JsonHas(ObjColumn,<span class="hljs-string">'owner.n*'</span>) = false
</code></pre>
<p>  由于是在SQLite3引擎内进行处理，并且使用了类似于SAX的快速方法（在搜索过程中不需要任何临时内存分配），因此这些JSON函数可能非常高效，可以与一些专用的NoSQL引擎相媲美。</p>
<p>  <em>译注：SAX，全称Simple API for XML，既是一种接口，也是一种软件包。它是一种XML解析的替代方法。SAX不同于DOM解析，它逐行扫描文档，一边扫描一边解析。由于应用程序只是在读取数据时检查数据，因此不需要将数据存储在内存中，这对于大型文档的解析是个巨大优势。</em></p>
<h6 id="toc_33">5.5.2.1.2.2. 动态数组字段<a class="vnote-anchor" href="#toc_33" data-anchor-icon="#"></a></h6>
<p>5.5.2.1.2.2.1. 基于Delphi的动态数组</p>
<p>  例如，下面是框架中包含的回归测试，定义了带有一些动态数组字段的<code>TSQLRecord</code>类：</p>
<pre><code class="lang-pascal hljs">TFV = <span class="hljs-keyword">packed</span> <span class="hljs-keyword">record</span>
    Major, Minor, Release, Build: integer;
    Main, Detailed: <span class="hljs-keyword">string</span>;
<span class="hljs-keyword">end</span>;
TFVs = <span class="hljs-keyword">array</span> <span class="hljs-keyword">of</span> TFV;
<span class="hljs-title">TSQLRecordPeopleArray</span> = <span class="hljs-keyword">class</span>(TSQLRecordPeople)
<span class="hljs-keyword">private</span>
  fInts: TIntegerDynArray;
  fCurrency: TCurrencyDynArray;
  fFileVersion: TFVs;
  fUTF8: RawUTF8;
<span class="hljs-keyword">published</span>
  <span class="hljs-keyword">property</span> UTF8: RawUTF8 <span class="hljs-keyword">read</span> fUTF8 <span class="hljs-keyword">write</span> fUTF8;
  <span class="hljs-keyword">property</span> Ints: TIntegerDynArray <span class="hljs-keyword">index</span> <span class="hljs-number">1</span> <span class="hljs-keyword">read</span> fInts <span class="hljs-keyword">write</span> fInts;
  <span class="hljs-keyword">property</span> Currency: TCurrencyDynArray <span class="hljs-keyword">index</span> <span class="hljs-number">2</span> <span class="hljs-keyword">read</span> fCurrency <span class="hljs-keyword">write</span> fCurrency;
  <span class="hljs-keyword">property</span> FileVersion: TFVs <span class="hljs-keyword">index</span> <span class="hljs-number">3</span> <span class="hljs-keyword">read</span> fFileVersion <span class="hljs-keyword">write</span> fFileVersion;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  这个<code>TSQLRecordPeopleArray</code>类继承自<code>TSQLRecordPeople</code>，因此它在父类字段（<code>FirstName</code>、<code>LastName</code>、<code>Data</code>、<code>YearOfBirth</code>、<code>YearOfDeath</code>）基础上又增加了一些<code>UTF8</code>、<code>Ints</code>、<code>Currency</code>和<code>FileVersion</code>类型的字段。</p>
<p>  通过下面的代码向<code>PeopleArray</code>表中添加了一些内容：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> V: TSQLRecordPeople;
    VA: TSQLRecordPeopleArray;
    FV: TFV;
  (...)
  V2.FillPrepare(Client,<span class="hljs-string">'LastName=:(''Dali''):'</span>);
  n := <span class="hljs-number">0</span>;
  <span class="hljs-keyword">while</span> V2.FillOne <span class="hljs-keyword">do</span>
  <span class="hljs-keyword">begin</span>
    VA.FillFrom(V2); <span class="hljs-comment">// fast copy some content from TSQLRecordPeople</span>
</code></pre>
<p>  <code>FillPrepare</code>/<code>FillOne</code>方法用于遍历所有<code>People</code>表，其<code>LastName</code>值为<code>'Dali'</code>（使用<code>:():</code>准备语句），然后使用<code>FillFrom</code>方法用这些值初始化<code>TSQLRecordPeopleArray</code>实例。</p>
<pre><code class="lang-pascal hljs">    inc(n);
    <span class="hljs-keyword">if</span> n <span class="hljs-keyword">and</span> <span class="hljs-number">31</span>=<span class="hljs-number">0</span> <span class="hljs-keyword">then</span>
    <span class="hljs-keyword">begin</span>
      VA.UTF8 := <span class="hljs-string">''</span>;
      VA.DynArray(<span class="hljs-string">'Ints'</span>).Add(n);
      Curr := n*<span class="hljs-number">0.01</span>;
      VA.DynArray(<span class="hljs-number">2</span>).Add(Curr);
      FV.Major := n;
      FV.Minor := n+<span class="hljs-number">2000</span>;
      FV.Release := n+<span class="hljs-number">3000</span>;
      FV.Build := n+<span class="hljs-number">4000</span>;
      str(n,FV.Main);
      str(n+<span class="hljs-number">1000</span>,FV.Detailed);
      VA.DynArray(<span class="hljs-string">'FileVersion'</span>).Add(FV);
    <span class="hljs-keyword">end</span> <span class="hljs-keyword">else</span>
      str(n,VA.fUTF8);
</code></pre>
<p>  变量<code>n</code>用于跟踪<code>PeopleArray</code>数字，并且大多数时候会在UTF8列中设置它的文本转换值，每32行就会向<code>VA</code>和<code>FV</code>动态数组字段添加一条记录。</p>
<p>  我们可以正常访问<code>VA</code>和<code>FV</code>动态数组，如下所示：</p>
<pre><code class="lang-pascal hljs">     SetLength(VA.Ints,length(VA.Ints)+<span class="hljs-number">1</span>);
     VA.Ints[high(VA.Ints)] := n;
</code></pre>
<p>  如果使用<code>DynArray</code>方法，允许通过<code>TDynArray</code>包装器直接访问动态数组。因此，上述的两行代码的与以下代码相同：</p>
<pre><code class="lang-pascal hljs">      VA.DynArray(<span class="hljs-string">'Ints'</span>).Add(n);
</code></pre>
<p>  注意，<code>DynArray</code>方法可以通过两个重载的参数来使用：字段名(<code>'Ints'</code>)或索引值，这是在类定义中定义的，所以我们也可以这样写：</p>
<pre><code class="lang-pascal hljs">      VA.DynArray(<span class="hljs-number">1</span>).Add(n);
</code></pre>
<p>  因为<code>Ints</code>字段被定义为：</p>
<pre><code class="lang-pascal hljs">    <span class="hljs-keyword">property</span> Ints: TIntegerDynArray
      <span class="hljs-keyword">index</span> <span class="hljs-number">1</span>
      <span class="hljs-keyword">read</span> fInts <span class="hljs-keyword">write</span> fInts;
</code></pre>
<p>  同样，下面一行将向货币字段添加货币值：</p>
<pre><code class="lang-pascal hljs">      VA.DynArray(<span class="hljs-number">2</span>).Add(Curr);
</code></pre>
<p>  在<code>FileVersion</code>字段中添加了更复杂的<code>TFV</code>记录的动态数组：</p>
<pre><code class="lang-pascal hljs">      VA.DynArray(<span class="hljs-string">'FileVersion'</span>).Add(FV);
</code></pre>
<p>  当然，使用<code>DynArray</code>方法比直接使用<code>SetLength/Ints[]</code>要慢一些。使用带索引的<code>DynArray</code>也应该比使用带文本字段名称的<code>DynArray</code>快一些(比如<code>Ints</code>)，但键入属性名称的方式又可能会减少键盘错误。如果需要快速大量向动态数组添加内容，可以使用带关联外部计数值的自定义TDynArray包装器，或者直接访问其内容（比如<code>SetLength + Ints[]</code>）</p>
<p>  最后在<code>FillPrepare/FillOne</code>循环体内用如下行结束：</p>
<pre><code class="lang-pascal hljs">    Check(Client.Add(VA,true)=n);
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  最后向数据库中添加<code>VA</code>字段内容，这将在<code>PeopleArray</code>表中创建新行，创建<code>ID</code>并与<code>n</code>变量的值比较。所有动态数组字段都将作为BLOB序列化存储到数据库表中。</p>
<p>5.5.2.1.2.2.2. 基于SQL的动态数组</p>
<p>  为了直接从SQL语句访问BLOB类型的动态数组，在<code>TSQLDataBase</code>中定义了一些新的SQL函数，以它们的类型名作为命名规则：</p>
<ul>
<li><code>ByteDynArrayContains(BlobField,I64)</code>;</li>
<li><code>WordDynArrayContains(BlobField,I64)</code>;</li>
<li><code>IntegerDynArrayContains(BlobField,I64)</code>;</li>
<li><code>CardinalDynArrayContains(BlobField,I64)</code>;</li>
<li><code>CurrencyDynArrayContains(BlobField,I64)</code> - 在这种情况下， <code>I64</code> 不是 <code>currency</code> 类型，不能直接用 <code>Int64</code> 参与运算（即不能用 <code>Int64(aCurrency)</code>），但可以先对 <code>currency</code> 进行调整，如 <code>aCurrency*10000</code> 或 <code>PInt64(@aCurrency)^</code>;</li>
<li><code>Int64DynArrayContains(BlobField,I64)</code>;</li>
<li><code>RawUTF8DynArrayContainsCase(BlobField,'Text')</code>;</li>
<li><code>RawUTF8DynArrayContainsNoCase(BlobField,'Text')</code>.</li>
</ul>
<p>  这些函数允许直接访问BLOB内容：</p>
<pre><code class="lang-pascal hljs">  <span class="hljs-keyword">for</span> i := <span class="hljs-number">1</span> <span class="hljs-keyword">to</span> n <span class="hljs-keyword">shr</span> <span class="hljs-number">5</span> <span class="hljs-keyword">do</span>
  <span class="hljs-keyword">begin</span>
    k := i <span class="hljs-keyword">shl</span> <span class="hljs-number">5</span>;
    aClient.OneFieldValues(TSQLRecordPeopleArray,<span class="hljs-string">'ID'</span>,
      FormatUTF8(<span class="hljs-string">'IntegerDynArrayContains(Ints,?)'</span>,[],[k]),IDs);
    Check(length(IDs)=n+<span class="hljs-number">1</span>-<span class="hljs-number">32</span>*i);
    <span class="hljs-keyword">for</span> j := <span class="hljs-number">0</span> <span class="hljs-keyword">to</span> high(IDs) <span class="hljs-keyword">do</span>
      Check(IDs[j]=k+j);
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  在上面的代码中，<code>OneFieldValues</code>方法的WHERE子句将使用SQL专用的<code>IntegerDynArrayContains</code> 函数在BLOB字段中检索<code>Ints</code>包含指定整数值k的所有记录。有了这样一个函数，所有的操作都在服务器端执行，没有缓慢的数据传输，也没有JSON/Base64序列化。</p>
<p>  例如，使用这样的SQL函数，您可以将多个<code>TSQLRecord.ID</code>字段值存储在一个<code>TIntegerDynArray</code>属性列中，并且在SQL语句中可以直接搜索，这可能是实现“一对多”或“多对多”关系的更好的方法，而不需要透视表。</p>
<p>  实现这些功能是为了提高速度。他们不会在搜索过程中创建任何临时的动态数组，但会直接访问内存中BLOB原始内容，这是SQlite引擎返回的内容。<code>RawUTF8DynArrayContainsCase/RawUTF8DynArrayContainsNoCase</code>函数也将直接在BLOB中搜索。对于请求量大的情况，这可能比使用<code>TSQLRecordMany</code>透视表要慢，因为搜索没有使用任何索引，并且在请求期间必须读取所有BLOB字段。但实际上，这些函数在相对较少的数据（最多可达50,000行）下运行得很好，SQlite3对BLOB字段访问进行了很好优化。</p>
<p>  对于处理更复杂的动态数组内容，您必须使用<code>TSQLDataBase. RegisterSQLFunction</code>创建自己的SQL函数，并关联<code>TSQLDataBaseSQLFunction</code>类，或定制服务或存储过程，如何实现请参阅下面。</p>
<h6 id="toc_34">5.5.2.1.2.3. TPersistent/TCollection字段<a class="vnote-anchor" href="#toc_34" data-anchor-icon="#"></a></h6>
<p>  例如，下面是框架中包含的回归测试案例，它定义了一个<code>TSQLRecord</code>类，添加了一些<code>TPersistent</code>、<code>TCollection</code>或<code>TRawUTF8List</code>字段（<code>TRawUTF8List</code>只是一个类似<code>TstringList</code>的组件，用于处理<code>RawUTF8</code>类型的字符串）：</p>
<pre><code class="lang-pascal hljs">  <span class="hljs-title">TSQLRecordPeopleObject</span> = <span class="hljs-keyword">class</span>(TSQLRecordPeople)
  <span class="hljs-keyword">private</span>
    fPersistent: TCollTst;
    fUTF8: TRawUTF8List;
  <span class="hljs-keyword">public</span>
    <span class="hljs-function"><span class="hljs-keyword">constructor</span> <span class="hljs-title">Create</span>;</span> <span class="hljs-keyword">override</span>;
    <span class="hljs-function"><span class="hljs-keyword">destructor</span> <span class="hljs-title">Destroy</span>;</span> <span class="hljs-keyword">override</span>;
  <span class="hljs-keyword">published</span>
    <span class="hljs-keyword">property</span> UTF8: TRawUTF8List <span class="hljs-keyword">read</span> fUTF8;
    <span class="hljs-keyword">property</span> Persistent: TCollTst <span class="hljs-keyword">read</span> fPersistent;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  为了避免出现内存泄漏或访问冲突，类实例初始化后还必须释放，需要在构造函数和析构函数中处理涉及到的所有实例：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">constructor</span> <span class="hljs-title">TSQLRecordPeopleObject</span>.<span class="hljs-title">Create</span>;</span>
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">inherited</span>;
  fPersistent := TCollTst.Create;
  fUTF8 := TRawUTF8List.Create;
<span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">destructor</span> <span class="hljs-title">TSQLRecordPeopleObject</span>.<span class="hljs-title">Destroy</span>;</span>
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">inherited</span>;
  FreeAndNil(fPersistent);
  FreeAndNil(fUTF8);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  下面是回归测试的具体实现：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> VO: TSQLRecordPeopleObject;
  (...)
<span class="hljs-keyword">if</span> Client.TransactionBegin(TSQLRecordPeopleObject) <span class="hljs-keyword">then</span>
<span class="hljs-keyword">try</span>
  V2.FillPrepare(Client,<span class="hljs-string">'LastName=?'</span>,[<span class="hljs-string">'Morse'</span>]);
  n := <span class="hljs-number">0</span>;
  <span class="hljs-keyword">while</span> V2.FillOne <span class="hljs-keyword">do</span>
  <span class="hljs-keyword">begin</span>
    VO.FillFrom(V2); <span class="hljs-comment">// fast copy some content from TSQLRecordPeople</span>
    inc(n);
    VO.Persistent.One.Color := n+<span class="hljs-number">100</span>;
    VO.Persistent.One.Length := n;
    VO.Persistent.One.<span class="hljs-keyword">Name</span> := Int32ToUtf8(n);
    <span class="hljs-keyword">if</span> n <span class="hljs-keyword">and</span> <span class="hljs-number">31</span>=<span class="hljs-number">0</span> <span class="hljs-keyword">then</span>
    <span class="hljs-keyword">begin</span>
      VO.UTF8.Add(VO.Persistent.One.<span class="hljs-keyword">Name</span>);
      <span class="hljs-keyword">with</span> VO.Persistent.Coll.Add <span class="hljs-keyword">do</span>
      <span class="hljs-keyword">begin</span>
        Color := n+<span class="hljs-number">1000</span>;
        Length := n*<span class="hljs-number">2</span>;
        <span class="hljs-keyword">Name</span> := Int32ToUtf8(n*<span class="hljs-number">3</span>);
      <span class="hljs-keyword">end</span>;
    <span class="hljs-keyword">end</span>;
    Check(Client.Add(VO,true)=n);
  <span class="hljs-keyword">end</span>;
  Client.Commit;
<span class="hljs-keyword">except</span>
  Client.RollBack; <span class="hljs-comment">// in case of error</span>
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  将向<code>PeopleObject</code>表添加1000行。</p>
<p>  首先，对记录的添加嵌套在事务过程里面，通过<code>TransactionBegin</code>和<code>Commit</code>方法可以加速SQL <code>INSERT</code>速度。请注意，<code>TransactionBegin</code>方法会返回一个布尔值，在多线程或客户端-服务端环境中应检查返回值（对本测试案例，内容都是在同一个线程中访问的，因此检查结果并不是强制性的，在这里这样做是为案例的准确性）。在框架的实现中，一般不使用事务嵌套，事务的典型使用应该是：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">if</span> Client.TransactionBegin(TSQLRecordPeopleObject) <span class="hljs-keyword">then</span>
<span class="hljs-keyword">try</span>
  <span class="hljs-comment">//.... modify the database content, raise exceptions on error</span>
  Client.Commit;
<span class="hljs-keyword">except</span>
  Client.RollBack; <span class="hljs-comment">// in case of error</span>
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  在有多个客户端并发交易的客户端-服务端环境中，可以使用专用的<code>TSQLRestClientURI.TransactionBeginRetry</code>方法：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">if</span> Client.TransactionBeginRetry(TSQLRecordPeopleObject,<span class="hljs-number">20</span>) <span class="hljs-keyword">then</span>
  ...
</code></pre>
<p>  请注意，客户端开启事务处理后，应该保证事务执行尽可能快（例如使用批处理等方式），因为其他客户端的所有写操作都需要等待事务被释放（通过提交或回滚完成事务处理）。</p>
<p>  对<code>TSQLRecord</code>派生类中的字段，通过调用<code>FillPrepare</code>/<code>FillOne</code>方法检索姓氏为<code>'Morse'</code>的行，设置<code>TPersistent</code>属性的<code>One</code>实例值（<code>VO.Persistent.One</code>），然后，每32行，向<code>VO.Persistent.Coll</code>添加一个新项。</p>
<p>  下面是<code>ID=32</code>时发送给服务器的实例数据：</p>
<pre><code class="lang-json hljs">{<span class="hljs-attr">"FirstName"</span>:<span class="hljs-string">"Samuel Finley Breese31"</span>,
<span class="hljs-attr">"LastName"</span>:<span class="hljs-string">"Morse"</span>,
<span class="hljs-attr">"YearOfBirth"</span>:<span class="hljs-number">1791</span>,
<span class="hljs-attr">"YearOfDeath"</span>:<span class="hljs-number">1872</span>,
<span class="hljs-attr">"UTF8"</span>:[<span class="hljs-string">"32"</span>],
<span class="hljs-attr">"Persistent"</span>:{<span class="hljs-attr">"One"</span>:{<span class="hljs-attr">"Color"</span>:<span class="hljs-number">132</span>,<span class="hljs-attr">"Length"</span>:<span class="hljs-number">32</span>,<span class="hljs-attr">"Name"</span>:<span class="hljs-string">"32"</span>},<span class="hljs-attr">"Coll"</span>:[{<span class="hljs-attr">"Color"</span>:<span class="hljs-number">1032</span>,<span class="hljs-attr">"Length"</span>:<span class="hljs-number">64</span>,<span class="hljs-attr">"Name"</span>:<span class="hljs-string">"96"</span>}]}
}
</code></pre>
<p>  在框架的1.15版本以前，传输的JSON内容不是一个真正的JSON对象，而是作为RawUTF8文本值（如双引号(<code>"</code>)字符转义成<code>“UTF8":"["32"]”</code>）。从框架的1.16版本开始，传输的数据是一个真正的JSON对象，以便更好地与AJAX客户机集成。也就是说，UTF8字段是作为一个有效的JSON字符串数组传输的，而<code>Persistent</code>字段是一个嵌套对象和数组的JSON对象。</p>
<p>  当将所有1000行添加到数据库文件中后，将执行如下循环，每次远程客户端都要连接到数据库引擎请求数据（通过任何可用的连接协议）：</p>
<pre><code class="lang-pascal hljs">  <span class="hljs-keyword">for</span> i := <span class="hljs-number">1</span> <span class="hljs-keyword">to</span> n <span class="hljs-keyword">do</span>
  <span class="hljs-keyword">begin</span>
    VO.ClearProperties;
    Client.Retrieve(i,VO);
    Check(VO.ID=i);
    Check(VO.LastName=<span class="hljs-string">'Morse'</span>);
    Check(VO.UTF8.Count=i <span class="hljs-keyword">shr</span> <span class="hljs-number">5</span>);
    <span class="hljs-keyword">for</span> j := <span class="hljs-number">0</span> <span class="hljs-keyword">to</span> VO.UTF8.Count-<span class="hljs-number">1</span> <span class="hljs-keyword">do</span>
      Check(GetInteger(pointer(VO.UTF8[j]))=(j+<span class="hljs-number">1</span>) <span class="hljs-keyword">shl</span> <span class="hljs-number">5</span>);
    Check(VO.Persistent.One.Length=i);
    Check(VO.Persistent.One.Color=i+<span class="hljs-number">100</span>);
    Check(GetInteger(pointer(VO.Persistent.One.<span class="hljs-keyword">Name</span>))=i);
    Check(VO.Persistent.Coll.Count=i <span class="hljs-keyword">shr</span> <span class="hljs-number">5</span>);
    <span class="hljs-keyword">for</span> j := <span class="hljs-number">0</span> <span class="hljs-keyword">to</span> VO.Persistent.Coll.Count-<span class="hljs-number">1</span> <span class="hljs-keyword">do</span>
     <span class="hljs-keyword">with</span> VO.Persistent.Coll[j] <span class="hljs-keyword">do</span>
     <span class="hljs-keyword">begin</span>
       k := (j+<span class="hljs-number">1</span>) <span class="hljs-keyword">shl</span> <span class="hljs-number">5</span>;
       Check(Color=k+<span class="hljs-number">1000</span>);
       Check(Length=k*<span class="hljs-number">2</span>);
       Check(GetInteger(pointer(<span class="hljs-keyword">Name</span>))=k*<span class="hljs-number">3</span>);
     <span class="hljs-keyword">end</span>;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  最神奇的是<code>Client.Retrieve(i,VO)</code>方法，它从数据库中检索数据为文本值，然后从JSON数组或对象中反序列化到内部的<code>TRawUTF8List</code>和<code>TPersistent</code>实例中。</p>
<p>  当检索<code>ID=33</code>行时，从服务器将接收到的JSON内容:</p>
<pre><code class="lang-json hljs">{<span class="hljs-attr">"ID"</span>:<span class="hljs-number">33</span>,
<span class="hljs-attr">"FirstName"</span>:<span class="hljs-string">"Samuel Finley Breese32"</span>,
<span class="hljs-attr">"LastName"</span>:<span class="hljs-string">"Morse"</span>,
<span class="hljs-attr">"YearOfBirth"</span>:<span class="hljs-number">1791</span>,
<span class="hljs-attr">"YearOfDeath"</span>:<span class="hljs-number">1872</span>,
<span class="hljs-attr">"UTF8"</span>:<span class="hljs-string">"[\"32\"]"</span>,
<span class="hljs-attr">"Persistent"</span>:<span class="hljs-string">"{\"One\":{\"Color\":133,\"Length\":33,\"Name\":\"33\"},\"Coll\":[{\"Color\":1032,\"Length\":64,\"Name\":\"96\"}]}"</span>}
</code></pre>
<p>  与POST内容相矛盾的是，它没有使用有效的嵌套JSON对象或数组，而是将<code>UTF8</code>和<code>Persistent</code>字段作为JSON字符串传输，这是框架的一个已知限制，因为从数据库中直接检索要比直接处理文本要快得多。对于AJAX应用程序，使用临时字符串并从中计算JSON内容并替换为相应的对象属性并不困难。在未来，这个实现可能会发生变化。</p>
<h6 id="toc_35">5.5.2.1.2.4. TObject / TObjectList 字段<a class="vnote-anchor" href="#toc_35" data-anchor-icon="#"></a></h6>
<p>  不仅<code>TPersistent</code>、<code>TCollection</code>和<code>TSQLRecord</code>类型可以通过写入所有发布字段来序列化，mORMot的ORM核心还使用<code>ObjectToJSON()</code>和<code>JSONToObject()</code>（又名<code>TJSONSerializer.WriteObject</code>）函数来处理JSON序列化。</p>
<p>  您有两个方法为类注册JSON序列化：</p>
<ul>
<li>自定义序列化的读写回调函数，参见下面的<code>TJSONSerializer.RegisterCustomSerializer</code>；</li>
<li>在调用<code>TJSONSerializer.RegisterClassForJSON</code>后，使用<code>TObjectList</code>实例。</li>
</ul>
<p>  在数据库中，这种对象将以文本（序列化为JSON）的形式存储，在客户端-服务端模式下以正常的JSON对象或数组的形式进行传输。</p>
<h6 id="toc_36">5.5.2.1.2.5. 在NoSQL引擎上分片<a class="vnote-anchor" href="#toc_36" data-anchor-icon="#"></a></h6>
<p>  我们的“无共享架构”与NoSQL的对象-文档映射(ODM)设计完全相同。</p>
<p>  事实上，mORMot与MongoDB的集成已经做了优化，任何高级属性（比如动态数组、变体和<code>TDocVariant</code>或类）都将作为BSON文档存储在MongoDB服务器。</p>
<p>  如果这些类型能够被序列化为JSON（对于普通类型、变体和动态数组/自定义记录类型均等），请参见下面，那么<code>mORMotDB.pas</code>单元将在服务器端以BSON对象或数组的形式存储此数据，而不是以BLOB或JSON文本（正如SQL后端一样）的形式存储，您将能够使用名称查询MongoDB集合中的任何嵌套子文档或子数组。</p>
<p>  因此，使用mORMot将数据分片将得益于RDBMS后端，这是一个可靠且经过验证的解决方案，同时也符合最新的NoSQL技术。</p>
<h4 id="toc_37">5.5.2.2. ORM透视表的实现<a class="vnote-anchor" href="#toc_37" data-anchor-icon="#"></a></h4>
<p>  从ORM的观点来看，数据分片是理所当然的。</p>
<p>  但定义一个数据透视表是关系型数据库的一种经典而强大的用法，它将体现它的强大：</p>
<ul>
<li>当数据很大时，您只需查询所需的数据，而不需要加载整个内容（这类似于ORM术语中的延迟加载）；</li>
<li>在主/从数据模型中，有时可以方便地直接访问详情记录，如数据整合；</li>
<li>最后但尤其重要的一点是，数据透视表是存储“has many through”关系（如关联时间或相应权限）数据的最自然的方式。</li>
</ul>
<h5 id="toc_38">5.5.2.2.1. TSQLRecordMany介绍<a class="vnote-anchor" href="#toc_38" data-anchor-icon="#"></a></h5>
<p>  我们已经创建了一个<code>TSQLRecordMany</code>专用类，它继承自标准<code>TSQLRecord</code>类（这是ORM存储的所有对象的基础），这个表格将把“多对多”关系转换成两个“一对多”关系。它应该至少包含两个<code>TSQLRecord</code>（即<code>INTEGER</code>）字段，命名为<code>“Source”</code>和<code>“Dest”</code>（名称是强制性的，因为ORM将利用它做精确匹配）。第一个指向源记录(<code>TSQLRecordMany</code>的一个属性字段)，第二个指向目标记录。</p>
<p>  例如：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-title">TSQLDest</span> = <span class="hljs-keyword">class</span>(TSQLRecord);
TSQLSource = <span class="hljs-keyword">class</span>;
<span class="hljs-title">TSQLDestPivot</span> = <span class="hljs-keyword">class</span>(TSQLRecordMany)
<span class="hljs-keyword">private</span>
  fSource: TSQLSource;
  fDest: TSQLDest;
  fTime: TDateTime;
<span class="hljs-keyword">published</span>
  <span class="hljs-keyword">property</span> Source: TSQLSource <span class="hljs-keyword">read</span> fSource; <span class="hljs-comment">// map Source column</span>
  <span class="hljs-keyword">property</span> Dest: TSQLDest <span class="hljs-keyword">read</span> fDest; <span class="hljs-comment">// map Dest column</span>
  <span class="hljs-keyword">property</span> AssociationTime: TDateTime <span class="hljs-keyword">read</span> fTime <span class="hljs-keyword">write</span> fTime;
<span class="hljs-keyword">end</span>;
<span class="hljs-title">TSQLSource</span> = <span class="hljs-keyword">class</span>(TSQLRecordSigned)
<span class="hljs-keyword">private</span>
  fDestList: TSQLDestPivot;
<span class="hljs-keyword">published</span>
  <span class="hljs-keyword">property</span> SignatureTime;
  <span class="hljs-keyword">property</span> Signature;
  <span class="hljs-keyword">property</span> DestList: TSQLDestPivot <span class="hljs-keyword">read</span> fDestList;
<span class="hljs-keyword">end</span>;
<span class="hljs-title">TSQLDest</span> = <span class="hljs-keyword">class</span>(TSQLRecordSigned)
<span class="hljs-keyword">published</span>
  <span class="hljs-keyword">property</span> SignatureTime;
  <span class="hljs-keyword">property</span> Signature;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  当<code>TSQLRecordMany</code>字段属性继承自<code>TSQLRecord</code>时，它将在执行<code>TSQLRecord.Create</code>构造函数时自动初始化。注意，<code>TSQLRecord</code>属性字段默认是一个整数值，该整数值是创建的“一对一”或“多对一”关系对应记录的<code>ID</code>值，但<code>TSQLRecordMany</code>是一个特例，所以不要迷惑！）</p>
<p>  这个<code>TSQLRecordMany</code>实例可以直接访问数据透视表记录，在<code>FillMany</code>后使用<code>FillRow</code>、<code>FillOne</code>和<code>FillRewind</code>方法来遍历记录，或<code>FillManyFromDest</code>/<code>DestGetJoin</code>更高级的用法。</p>
<p>  下面是在<code>SynSelfTests</code>单元中编写的回归测试代码：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TestMany</span><span class="hljs-params">(aClient: TSQLRestClient)</span>;</span>
<span class="hljs-keyword">var</span> MS: TSQLSource;
    MD, MD2: TSQLDest;
    i: integer;
    sID, dID: <span class="hljs-keyword">array</span>[<span class="hljs-number">1</span>..<span class="hljs-number">100</span>] <span class="hljs-keyword">of</span> Integer;
    res: TIntegerDynArray;
<span class="hljs-keyword">begin</span>
  MS := TSQLSource.Create;
  MD := TSQLDest.Create;
  <span class="hljs-keyword">try</span>
    MD.fSignatureTime := TimeLogNow;
    MS.fSignatureTime := MD.fSignatureTime;
    Check(MS.DestList&lt;&gt;<span class="hljs-keyword">nil</span>);
    Check(MS.DestList.InheritsFrom(TSQLRecordMany));
    aClient.TransactionBegin(TSQLSource); <span class="hljs-comment">// faster process</span>
</code></pre>
<p>  这段代码将创建<code>TSQLSource</code>/<code>TSQLDest</code>两个实例，然后开启一个事务（事务可以加速数据库引擎进程，因为一次会添加多个记录）。注意，在执行<code>TSQLSource.Create</code>构造函数时，检测到<code>TSQLRecordMany</code>字段的存在，将使用<code>TSQLDestPivot</code>实例数据填充<code>DestList</code>属性。因此，这个<code>DestList</code>属性可以使用“has-many”专用方法，如<code>ManyAdd</code>。</p>
<pre><code class="lang-pascal hljs">    <span class="hljs-keyword">for</span> i := <span class="hljs-number">1</span> <span class="hljs-keyword">to</span> high(dID) <span class="hljs-keyword">do</span>
    <span class="hljs-keyword">begin</span>
      MD.fSignature := FormatUTF8(<span class="hljs-string">'% %'</span>,[aClient.ClassName,i]);
      dID[i] := aClient.Add(MD,true);
      Check(dID[i]&gt;<span class="hljs-number">0</span>);
    <span class="hljs-keyword">end</span>;
</code></pre>
<p>  下面将向<code>Dest</code>表添加一些行。</p>
<pre><code class="lang-pascal hljs">    <span class="hljs-keyword">for</span> i := <span class="hljs-number">1</span> <span class="hljs-keyword">to</span> high(sID) <span class="hljs-keyword">do</span> <span class="hljs-keyword">begin</span>
      MS.fSignature := FormatUTF8(<span class="hljs-string">'% %'</span>,[aClient.ClassName,i]);
      sID[i] := aClient.Add(MS,True);
      Check(sID[i]&gt;<span class="hljs-number">0</span>);
      MS.DestList.AssociationTime := i;
      Check(MS.DestList.ManyAdd(aClient,sID[i],dID[i])); <span class="hljs-comment">// associate both lists</span>
      Check(<span class="hljs-keyword">not</span> MS.DestList.ManyAdd(aClient,sID[i],dID[i],true)); <span class="hljs-comment">// no dup</span>
    <span class="hljs-keyword">end</span>;
    aClient.Commit;
</code></pre>
<p>  这将创建一些主表记录，并调用自动创建的<code>DestList</code>实例的<code>ManyAdd</code>方法，将从表记录与主表记录关联起来，从而实现一个“has many through”关系，并设置<code>AssociationTime</code>字段。</p>
<p>  然后将事务提交数据库。</p>
<pre><code class="lang-pascal hljs">    <span class="hljs-keyword">for</span> i := <span class="hljs-number">1</span> <span class="hljs-keyword">to</span> high(dID) <span class="hljs-keyword">do</span>
    <span class="hljs-keyword">begin</span>
      Check(MS.DestList.SourceGet(aClient,dID[i],res));
      <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> Check(length(res)=<span class="hljs-number">1</span>) <span class="hljs-keyword">then</span>
        Check(res[<span class="hljs-number">0</span>]=sID[i]);
      Check(MS.DestList.ManySelect(aClient,sID[i],dID[i]));
      Check(MS.DestList.AssociationTime=i);
    <span class="hljs-keyword">end</span>;
</code></pre>
<p>  上面这段代码将验主/从表的关联关系，使用专用的<code>SourceGet</code>方法检索与从表<code>ID</code>相关联的所有主表<code>ID</code>，即一条记录关联的<code>sID[]</code>值，它还会检查“has many through”关系的<code>AssociationTime</code>。</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">for</span> i := <span class="hljs-number">1</span> <span class="hljs-keyword">to</span> high(sID) <span class="hljs-keyword">do</span>
<span class="hljs-keyword">begin</span>
  Check(MS.DestList.DestGet(aClient,sID[i],res));
  <span class="hljs-keyword">if</span> Check(length(res)=<span class="hljs-number">1</span>) <span class="hljs-keyword">then</span>
    <span class="hljs-keyword">continue</span>; <span class="hljs-comment">// avoid GPF</span>
  Check(res[<span class="hljs-number">0</span>]=dID[i]);
</code></pre>
<p>  <code>DestGet</code>方法检索与主表记录<code>ID</code>关联的所有从表记录<code>ID</code>，即一条记录，对应<code>dID[]</code>的多个值。</p>
<pre><code class="lang-pascal hljs">  Check(MS.DestList.FillMany(aClient,sID[i])=<span class="hljs-number">1</span>);
</code></pre>
<p>  上面的代码将从数据透视表检索所有与主表记录<code>ID</code>对应的从表<code>ID</code>并填充<code>DestList</code>实例，函数应该只返回一个条目。</p>
<pre><code class="lang-pascal hljs">  Check(MS.DestList.FillOne);
  Check(Integer(MS.DestList.Source)=sID[i]);
  Check(Integer(MS.DestList.Dest)=dID[i]);
  Check(MS.DestList.AssociationTime=i);
  Check(<span class="hljs-keyword">not</span> MS.DestList.FillOne);
</code></pre>
<p>  这些行将获取第一条且唯一的一台记录，检查主/从表和<code>AssociationTime</code>属性是否匹配预期值。再次调用<code>FillOne</code>应该会失败，因为匹配这个主表记录<code>ID</code>的记录只有一条。</p>
<pre><code class="lang-pascal hljs">  Check(MS.DestList.DestGetJoined(aClient,<span class="hljs-string">''</span>,sID[i],res));
  <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> Check(length(res)=<span class="hljs-number">1</span>) <span class="hljs-keyword">then</span>
    Check(res[<span class="hljs-number">0</span>]=dID[i]);
</code></pre>
<p>  上面代码将检索与主表记录<code>ID</code>相关联的所有从表记录<code>ID</code>，没有附带WHERE条件。</p>
<pre><code class="lang-pascal hljs">  Check(MS.DestList.DestGetJoined(aClient,<span class="hljs-string">'Dest.SignatureTime=:(0):'</span>,sID[i],res));
  Check(length(res)=<span class="hljs-number">0</span>);
</code></pre>
<p>  上面代码将检索与主表记录<code>ID</code>关联的所有从表记录<code>ID</code>，附加了一个始终无效的WHERE条件，<code>res</code>数组应该没有返回项，因为<code>SignatureTime</code>永远不等于0。</p>
<pre><code class="lang-pascal hljs">  Check(MS.DestList.DestGetJoined(aClient,
    FormatUTF8(<span class="hljs-string">'Dest.SignatureTime=?'</span>,[],[MD.SignatureTime]),sID[i],res));
  <span class="hljs-keyword">if</span> Check(length(res)=<span class="hljs-number">1</span>) <span class="hljs-keyword">then</span>
    <span class="hljs-keyword">continue</span>; <span class="hljs-comment">// avoid GPF</span>
  Check(res[<span class="hljs-number">0</span>]=dID[i]);
</code></pre>
<p>  上面代码将检索与主表记录<code>ID</code>关联的满足WHERE条件的所有从表记录<code>ID</code>，它应该返回一条记录。</p>
<p>  注意<code>FormatUTF8()</code>全局函数调用生成WHERE子句的代码，你也可以这样写：</p>
<pre><code class="lang-pascal hljs">  Check(MS.DestList.DestGetJoined(aClient,
    <span class="hljs-string">'Dest.SignatureTime=:('</span>+Int64ToUTF8(MD.SignatureTime)+<span class="hljs-string">'):'</span>,sID[i],res));
</code></pre>
<p>  这种<code>:(..):</code>内联参数的方法没有<code>'?'</code>方便，特别是对于字符串(<code>RawUTF8</code>)值。</p>
<pre><code class="lang-pascal hljs">  MD2 := MS.DestList.DestGetJoined(aClient,
    FormatUTF8(<span class="hljs-string">'Dest.SignatureTime=?'</span>,[],[MD.SignatureTime]),sID[i]) <span class="hljs-keyword">as</span> TSQLADest;
  <span class="hljs-keyword">if</span> Check(MD2&lt;&gt;<span class="hljs-keyword">nil</span>) <span class="hljs-keyword">then</span>
    <span class="hljs-keyword">continue</span>;
  <span class="hljs-keyword">try</span>
    Check(MD2.FillOne);
    Check(MD2.ID=dID[i]);
    Check(MD2.Signature=FormatUTF8(<span class="hljs-string">'% %'</span>,[aClient.ClassName,i]));
  <span class="hljs-keyword">finally</span>
    MD2.Free;
  <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  上面这个重载的<code>DestGetJoined</code>函数将返回<code>TSQLDest</code>实例到<code>MD2</code>，该实例拥有与主表记录<code>ID</code>相关联的满足WHERE条件的所有从表记录内容，然后使用<code>FillOne</code>获取第一条也是唯一的一条满足条件的从表记录，并检查它的值。</p>
<pre><code class="lang-pascal hljs">    aClient.TransactionBegin(TSQLADests); <span class="hljs-comment">// faster process</span>
    <span class="hljs-keyword">for</span> i := <span class="hljs-number">1</span> <span class="hljs-keyword">to</span> high(sID) <span class="hljs-keyword">shr</span> <span class="hljs-number">2</span> <span class="hljs-keyword">do</span>
      Check(MS.DestList.ManyDelete(aClient,sID[i*<span class="hljs-number">4</span>],dID[i*<span class="hljs-number">4</span>]));
    aClient.Commit;
    <span class="hljs-keyword">for</span> i := <span class="hljs-number">1</span> <span class="hljs-keyword">to</span> high(sID) <span class="hljs-keyword">do</span>
      <span class="hljs-keyword">if</span> i <span class="hljs-keyword">and</span> <span class="hljs-number">3</span>&lt;&gt;<span class="hljs-number">0</span> <span class="hljs-keyword">then</span>
      <span class="hljs-keyword">begin</span>
        Check(MS.DestList.ManySelect(aClient,sID[i],dID[i]));
        Check(MS.DestList.AssociationTime=i);
      <span class="hljs-keyword">end</span> <span class="hljs-keyword">else</span>
        Check(<span class="hljs-keyword">not</span> MS.DestList.ManySelect(aClient,sID[i],dID[i]));
</code></pre>
<p>  上面这段代码将从每4个关联关系中删除一个，并确保<code>ManySelect</code>只检索期望的关联。</p>
<pre><code class="lang-pascal hljs">  <span class="hljs-keyword">finally</span>
    MD.Free;
    MS.Free;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  最后释放实例，包括在<code>DestList</code>属性中创建的<code>TSQLDestPivot</code>实例。</p>
<h5 id="toc_39">5.5.2.2.2. JOIN查询自动化<a class="vnote-anchor" href="#toc_39" data-anchor-icon="#"></a></h5>
<p>  所有<code>ManySelect</code>、<code>DestGetJoined</code>…方法都用于从透视表检索表之间的关系，这可以节省带宽，并且适用于大多数情况，但不是处理多对多关系的唯一方法。在同一主表记录中可能对应多个<code>TSQLRecordMany</code>实例，这种情况下，这些方法就失效了。</p>
<p>  在SQL世界中，在数据库中基于主表创建关联查询并合并两个或多个表的记录是非常常见的，查询结果是一个可以保存为表或按原样使用的集合。JOIN是对两个或多个表相关联的字段值进行比对关联的方法，手工编写这样的联合语句并不容易，特别是需要使用多个表，需要准确指定要检索字段，如果你有几个透视表，它可能会成为一个噩梦。让我们看看ORM将如何处理它。</p>
<p>  框架在TSQLRecord类中添加了专用的<code>FillPrepareMany</code>方法，并添加了一个名为<code>CreateAndFillPrepareMany</code>的新的构造函数，这个特定的方法将实现：</p>
<ul>
<li>将每个<code>TSQLRecordMany</code>实例对应的<code>Dest</code>属性实例化，这样JOIN请求将能够直接处理这些值；</li>
<li>正确创建<code>SELECT</code>语句及其可选的WHERE子句。</li>
</ul>
<p>  下面代码是我们回归测试使用的案例，基于同一数据库环境：</p>
<pre><code class="lang-pascal hljs">Check(MS.FillPrepareMany(aClient,
  <span class="hljs-string">'DestList.Dest.SignatureTime&lt;&gt;% and id&gt;=? and DestList.AssociationTime&lt;&gt;0 '</span>+
  <span class="hljs-string">'and SignatureTime=DestList.Dest.SignatureTime '</span>+
  <span class="hljs-string">'and DestList.Dest.Signature&lt;&gt;"DestList.AssociationTime"'</span>,[<span class="hljs-number">0</span>],[sID[<span class="hljs-number">1</span>]]));
</code></pre>
<p>  这里唯一有用的参数是<code>id&gt;=?</code>，用于检索主表中刚刚添加的关系，所有其他条件都将始终为真，主要用于测试生成的SQL。</p>
<p>  上面的mORMot代码将生成以下SQL语句：</p>
<pre><code class="lang-sql hljs"><span class="hljs-keyword">select</span> A.ID AID,A.SignatureTime A00,A.Signature A01,
  B.ID BID,B.AssociationTime B02,
  C.ID CID,C.SignatureTime C00,C.Signature C01
<span class="hljs-keyword">from</span> ASource A,ADests B,ADest C
<span class="hljs-keyword">where</span> B.Source=A.ID <span class="hljs-keyword">and</span> B.Dest=C.ID
  <span class="hljs-keyword">and</span> (C.SignatureTime&lt;&gt;<span class="hljs-number">0</span> <span class="hljs-keyword">and</span> A.id&gt;=:(<span class="hljs-number">1</span>): <span class="hljs-keyword">and</span> B.AssociationTime&lt;&gt;<span class="hljs-number">0</span>
  <span class="hljs-keyword">and</span> A.SignatureTime=C.SignatureTime <span class="hljs-keyword">and</span> C.Signature&lt;&gt;<span class="hljs-string">"DestList.AssociationTime"</span>)
</code></pre>
<p>  您可以关注以下内容：</p>
<ul>
<li>SQL语句中包含所有声明的<code>TSQLRecordMany</code>实例（重命名为<code>B</code>）和对应的<code>Dest</code>实例（重命名为<code>C</code>）；</li>
<li>每个类的所有普通属性字段都定义了简短的唯一标识符（<code>AID</code>, <code>A01</code>, <code>BID</code>, <code>B02</code>…）作为别名；</li>
<li>创建了JOIN子句（<code>B.Source=A.ID and B.Dest=C.ID</code>）；</li>
<li>代码中的WHERE子句转换为正确的SQL语句，包括表的内部别名（<code>A</code>,<code>B</code>,<code>C</code>），事实上，<code>DestList.Dest</code>被<code>C</code>取代，主表<code>ID</code>属性被正确地声明为<code>A.ID</code>，<code>“DestList.AssociationTime”</code>的文本保持不变，因为它有引号。</li>
</ul>
<p>  也就是说，我们的ORM为你做了所有的苦活累活！您可以使用delphi级别的查询条件，ORM引擎将透明地将它们转换为有效的SQL语句，对于可能在实际应用程序中出现多个透视表的情况，这种优势会很明显。</p>
<p>  在准备好查询语句之后，您可以使用标准的<code>FillOne</code>方法遍历所有数据行，并使用Delphi对象实例访问JOIN的所有字段：</p>
<pre><code class="lang-pascal hljs">  Check(MS.FillTable.RowCount=length(sID));
  <span class="hljs-keyword">for</span> i := <span class="hljs-number">1</span> <span class="hljs-keyword">to</span> high(sID) <span class="hljs-keyword">do</span> <span class="hljs-keyword">begin</span>
   MS.FillOne;
    Check(MS.fID=sID[i]);
    Check(MS.SignatureTime=MD.fSignatureTime);
    Check(MS.DestList.AssociationTime=i);
    Check(MS.DestList.Dest.fID=dID[i]);
    Check(MS.DestList.Dest.SignatureTime=MD.fSignatureTime);
    Check(MS.DestList.Dest.Signature=FormatUTF8(<span class="hljs-string">'% %'</span>,[aClient.ClassName,i]));
  <span class="hljs-keyword">end</span>;
  MS.FillClose;
</code></pre>
<p>  在示例中，我们显示调用了<code>FillClose</code>方法，以释放<code>FillPrepareMany</code>中创建的所有<code>Dest</code>实例。这不是强制性的，你也可以直接调用<code>MS.Free</code>，但如果使用了一些常用的多对多的方法，如<code>MS.DestList.ManySelect()</code>，<code>FillClose</code>可避免出现一般性保护错误，能识别到<code>Dest</code>属性不是实例，而是<code>pointer(DestID)</code>指针值。</p>
<h2 id="toc_40">5.6. ORM数据模型<a class="vnote-anchor" href="#toc_40" data-anchor-icon="#"></a></h2>
<h3 id="toc_41">5.6.1. 创建ORM模型<a class="vnote-anchor" href="#toc_41" data-anchor-icon="#"></a></h3>
<p>  <code>TSQLModel</code>类聚合了应用程序需要的所有<code>TSQLRecord</code>派生类，包括数据库相关的类和与业务逻辑相关的类。</p>
<p>  遵循MVC模式，当您必须直接处理数据表时，需要用到<code>TSQLModel</code>实例，但不要尝试使用底层的<code>TSQLDataBase.GetTableNames</code>或<code>TSQLDataBase.GetFieldNames</code>方法。实际上，在模型中声明的表在SQLite3数据库中可能还不可用，需要通过<code>TSQLRestServer.StaticDataCreate</code>方法将其定义为<code>TSQLRestStorageInMemory</code>实例，或使用外部表。您甚至可以运行没有任何SQLite3引擎的mORMot服务器，仅使用纯粹的内存表！</p>
<p>  实际上，每个<code>TSQLModel</code>实例都会关联一个<code>TSQLRest</code>实例，使用<code>Owner</code>属性可以访问与此ORM模型关联的当前运行的客户端或服务端的<code>TSQLRest</code>实例。</p>
<p>  根据设计，ORM模型在客户端和服务端都要使用，因此，应该使用一个公共单元来定义所有<code>TSQLRecord</code>类型，并使用一个公共函数来创建相关的<code>TSQLModel</code>类。</p>
<p>  例如，下面是源代码仓库中第一个示例代码的<code>SampleData.pas</code>单元中定义的函数：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">CreateSampleModel</span>:</span> TSQLModel;
<span class="hljs-keyword">begin</span>
  result := TSQLModel.Create([TSQLSampleRecord]);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  有关更复杂的模型，包括其在用户界面的呈现，请参见下面。</p>
<h3 id="toc_42">5.6.2. 多个模型<a class="vnote-anchor" href="#toc_42" data-anchor-icon="#"></a></h3>
<p>  在实践中，同一个<code>TSQLRecord</code>可以在多个模型中使用，典型的如<code>TSQLAuthUser</code>表，或者客户端和服务端实例运行在同一个进程中时，因此，对模型属性的访问有两层结构：</p>
<table>
<thead>
<tr>
<th>类</th>
<th style="text-align:left">描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>TSQLModelRecordProperties</td>
<td style="text-align:left">模型的ORM参数，如用于SQL自动生成和外部数据库设置。用<code>TSQLModel.TableProps[]</code>数组访问这些实例。</td>
</tr>
<tr>
<td>TSQLRecordProperties</td>
<td style="text-align:left">表的底层属性，由mORMot的ORM内核获取。用<code>TSQLModel.TableProps[].Props</code>访问这些实例</td>
</tr>
</tbody>
</table>
<p>  所以你可以这样编写代码：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> i: integer;
    ModelProps: TSQLModelRecordProperties;
    Props: TSQLRecordProperties;
<span class="hljs-keyword">begin</span>
  ...
  <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span> <span class="hljs-keyword">to</span> high(Model.TableProps) <span class="hljs-keyword">do</span> <span class="hljs-keyword">begin</span>
    ModelProps := Model.TableProps[i];
    <span class="hljs-comment">// now you can access ModelProps.ExternalDB.TableName ...</span>
    Props := ModelProps.Props;
    <span class="hljs-comment">// now you can use Props.SQLTableName or Props.Fields[]</span>
  <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>;
</code></pre>
<h3 id="toc_43">5.6.3. 过滤与验证<a class="vnote-anchor" href="#toc_43" data-anchor-icon="#"></a></h3>
<p>  根据n层体系架构（参见<a href="">多层体系架构</a>），数据过滤和验证应该在业务逻辑中实现，而不是在用户界面中。</p>
<p>  如果您曾经使用Delphi开发过RAD数据库应用程序，那么您可能需要改变一下习惯。数据过滤和验证不应该在用户界面中实现，而应该在纯Delphi代码中实现。</p>
<p>  为了简化这个过程，<code>SynCommons.pas</code>单元中有一组专门的类，用于定义过滤(转换)和验证，他们都是如下两个类的派生类：</p>
<div class="mermaid-diagram"><svg id="mermaid-diagram-2" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 307 198" style="max-width:307px;"><style type="text/css" title="mermaid-svg-internal-css">/*  */
#mermaid-diagram-2 .node&gt;rect { ; }
#mermaid-diagram-2 .node text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-2 .edgeLabel text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-2 .cluster rect  { rx:4px; fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-2 .cyan&gt;rect, .cyan&gt;polygon, .cyan&gt;circle, .cyan&gt;ellipse { fill:#9ff;  stroke:#fff; }
/*  */
</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath" style="opacity: 1;"><path class="path" d="M74.5,64L74.5,89L113.99468085106383,114" marker-end="url(#arrowhead33)" style="fill:none"></path><defs><marker id="arrowhead33" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M223,64L223,89L183.50531914893617,114" marker-end="url(#arrowhead34)" style="fill:none"></path><defs><marker id="arrowhead34" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node cyan" id="A" transform="translate(74.5,42)" style="opacity: 1;"><rect rx="0" ry="0" x="-54.5" y="-22" width="109" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-44.5,-12)"><foreignObject width="89" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynValidate</div></foreignObject></g></g></g><g class="node cyan" id="C" transform="translate(148.75,136)" style="opacity: 1;"><rect rx="0" ry="0" x="-80.5" y="-22" width="161" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-70.5,-12)"><foreignObject width="141" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynFilterOrValidate</div></foreignObject></g></g></g><g class="node cyan" id="B" transform="translate(223,42)" style="opacity: 1;"><rect rx="0" ry="0" x="-44" y="-22" width="88" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-34,-12)"><foreignObject width="68" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynFilter</div></foreignObject></g></g></g></g></g></g></svg></div>
<p>  <code>TSQLRecord</code>字段内容的过滤在<code>TSQLRecord</code>中处理，使用虚拟过滤方法，或通过一些<code>TSQLFilter</code>类，它们将根据一些规则转换对象字段，例如强制使用大写/小写，或对文本进行裁剪。</p>
<p>  <code>TSQLRecord</code>字段内容验证用<code>TSQLRecord.Validate</code>虚拟方法处理，或通过一些<code>TSQLValidate</code>类。在这里，将根据一组规则检查对象字段，报告无效内容。</p>
<p>  在<code>SynCommons.pas</code>和<code>mORMot.pas</code>单元定义了一些“标准”类，涵盖最常见的用途：</p>
<div class="mermaid-diagram"><svg id="mermaid-diagram-3" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 908 1232" style="max-width:908px;"><style type="text/css" title="mermaid-svg-internal-css">/*  */
#mermaid-diagram-3 .node&gt;rect { ; }
#mermaid-diagram-3 .node text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-3 .edgeLabel text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-3 .cluster rect  { rx:4px; fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-3 .cyan&gt;rect, .cyan&gt;polygon, .cyan&gt;circle, .cyan&gt;ellipse { fill:#9ff;  stroke:#fff; }
/*  */
</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath" style="opacity: 1;"><path class="path" d="M223.5,606L274,606L330,606" marker-end="url(#arrowhead67)" style="fill:none"></path><defs><marker id="arrowhead67" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M249,794L274,794L326,794" marker-end="url(#arrowhead68)" style="fill:none"></path><defs><marker id="arrowhead68" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M249,888L274,888L329,888" marker-end="url(#arrowhead69)" style="fill:none"></path><defs><marker id="arrowhead69" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M249,982L274,982L319,982" marker-end="url(#arrowhead70)" style="fill:none"></path><defs><marker id="arrowhead70" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M486.5,42L523,42L595.0574468085107,255" marker-end="url(#arrowhead71)" style="fill:none"></path><defs><marker id="arrowhead71" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M481,136L523,136L590.0957446808511,255" marker-end="url(#arrowhead72)" style="fill:none"></path><defs><marker id="arrowhead72" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M457.5,230L523,230L565.2872340425532,255" marker-end="url(#arrowhead73)" style="fill:none"></path><defs><marker id="arrowhead73" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M485.5,324L523,324L565.2872340425532,299" marker-end="url(#arrowhead74)" style="fill:none"></path><defs><marker id="arrowhead74" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M480,418L523,418L590.0957446808511,299" marker-end="url(#arrowhead75)" style="fill:none"></path><defs><marker id="arrowhead75" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M472.5,512L523,512L595.0574468085107,299" marker-end="url(#arrowhead76)" style="fill:none"></path><defs><marker id="arrowhead76" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M467,606L523,606L596.2978723404256,866" marker-end="url(#arrowhead77)" style="fill:none"></path><defs><marker id="arrowhead77" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M498,700L523,700L593.1968085106383,866" marker-end="url(#arrowhead78)" style="fill:none"></path><defs><marker id="arrowhead78" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M471,794L523,794L583.8936170212766,866" marker-end="url(#arrowhead79)" style="fill:none"></path><defs><marker id="arrowhead79" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M468,888L523,888L548,888" marker-end="url(#arrowhead80)" style="fill:none"></path><defs><marker id="arrowhead80" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M478,982L523,982L583.8936170212766,910" marker-end="url(#arrowhead81)" style="fill:none"></path><defs><marker id="arrowhead81" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M487.5,1076L523,1076L593.1968085106383,910" marker-end="url(#arrowhead82)" style="fill:none"></path><defs><marker id="arrowhead82" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M472,1170L523,1170L596.2978723404256,910" marker-end="url(#arrowhead83)" style="fill:none"></path><defs><marker id="arrowhead83" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M646.5,277L682,277L779.2695035460993,537" marker-end="url(#arrowhead84)" style="fill:none"></path><defs><marker id="arrowhead84" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M657,888L682,888L780.4452887537994,581" marker-end="url(#arrowhead85)" style="fill:none"></path><defs><marker id="arrowhead85" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node cyan" id="A1" transform="translate(134.5,606)" style="opacity: 1;"><rect rx="0" ry="0" x="-89" y="-22" width="178" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-79,-12)"><foreignObject width="158" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynValidatePassWord</div></foreignObject></g></g></g><g class="node cyan" id="B7" transform="translate(398.5,606)" style="opacity: 1;"><rect rx="0" ry="0" x="-68.5" y="-22" width="137" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-58.5,-12)"><foreignObject width="117" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynValidateText</div></foreignObject></g></g></g><g class="node cyan" id="A2" transform="translate(134.5,794)" style="opacity: 1;"><rect rx="0" ry="0" x="-114.5" y="-22" width="229" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-104.5,-12)"><foreignObject width="209" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynValidateTableUniqueField</div></foreignObject></g></g></g><g class="node cyan" id="B9" transform="translate(398.5,794)" style="opacity: 1;"><rect rx="0" ry="0" x="-72.5" y="-22" width="145" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-62.5,-12)"><foreignObject width="125" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynValidateTable</div></foreignObject></g></g></g><g class="node cyan" id="A3" transform="translate(134.5,888)" style="opacity: 1;"><rect rx="0" ry="0" x="-114.5" y="-22" width="229" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-104.5,-12)"><foreignObject width="209" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynValidateTableUniqueField</div></foreignObject></g></g></g><g class="node cyan" id="B10" transform="translate(398.5,888)" style="opacity: 1;"><rect rx="0" ry="0" x="-69.5" y="-22" width="139" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-59.5,-12)"><foreignObject width="119" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynValidateRest</div></foreignObject></g></g></g><g class="node cyan" id="A4" transform="translate(134.5,982)" style="opacity: 1;"><rect rx="0" ry="0" x="-114.5" y="-22" width="229" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-104.5,-12)"><foreignObject width="209" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynValidateTableUniqueField</div></foreignObject></g></g></g><g class="node cyan" id="B11" transform="translate(398.5,982)" style="opacity: 1;"><rect rx="0" ry="0" x="-79.5" y="-22" width="159" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-69.5,-12)"><foreignObject width="139" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynValidatePattern</div></foreignObject></g></g></g><g class="node cyan" id="B1" transform="translate(398.5,42)" style="opacity: 1;"><rect rx="0" ry="0" x="-88" y="-22" width="176" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-78,-12)"><foreignObject width="156" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynFilterUpperCaseU</div></foreignObject></g></g></g><g class="node cyan" id="C1" transform="translate(602.5,277)" style="opacity: 1;"><rect rx="0" ry="0" x="-44" y="-22" width="88" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-34,-12)"><foreignObject width="68" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynFilter</div></foreignObject></g></g></g><g class="node cyan" id="B2" transform="translate(398.5,136)" style="opacity: 1;"><rect rx="0" ry="0" x="-82.5" y="-22" width="165" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-72.5,-12)"><foreignObject width="145" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynFilterUpperCase</div></foreignObject></g></g></g><g class="node cyan" id="B3" transform="translate(398.5,230)" style="opacity: 1;"><rect rx="0" ry="0" x="-59" y="-22" width="118" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-49,-12)"><foreignObject width="98" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynFilterTrim</div></foreignObject></g></g></g><g class="node cyan" id="B4" transform="translate(398.5,324)" style="opacity: 1;"><rect rx="0" ry="0" x="-87" y="-22" width="174" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-77,-12)"><foreignObject width="154" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynFilterLowerCaseU</div></foreignObject></g></g></g><g class="node cyan" id="B5" transform="translate(398.5,418)" style="opacity: 1;"><rect rx="0" ry="0" x="-81.5" y="-22" width="163" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-71.5,-12)"><foreignObject width="143" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynFilterLowerCase</div></foreignObject></g></g></g><g class="node cyan" id="B6" transform="translate(398.5,512)" style="opacity: 1;"><rect rx="0" ry="0" x="-74" y="-22" width="148" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-64,-12)"><foreignObject width="128" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynFilterTruncate</div></foreignObject></g></g></g><g class="node cyan" id="C2" transform="translate(602.5,888)" style="opacity: 1;"><rect rx="0" ry="0" x="-54.5" y="-22" width="109" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-44.5,-12)"><foreignObject width="89" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynValidate</div></foreignObject></g></g></g><g class="node cyan" id="B8" transform="translate(398.5,700)" style="opacity: 1;"><rect rx="0" ry="0" x="-99.5" y="-22" width="199" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-89.5,-12)"><foreignObject width="179" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynValidateNonVoidText</div></foreignObject></g></g></g><g class="node cyan" id="B12" transform="translate(398.5,1076)" style="opacity: 1;"><rect rx="0" ry="0" x="-89" y="-22" width="178" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-79,-12)"><foreignObject width="158" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynValidateIPAddress</div></foreignObject></g></g></g><g class="node cyan" id="B13" transform="translate(398.5,1170)" style="opacity: 1;"><rect rx="0" ry="0" x="-73.5" y="-22" width="147" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-63.5,-12)"><foreignObject width="127" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynValidateEmail</div></foreignObject></g></g></g><g class="node cyan" id="D" transform="translate(787.5,559)" style="opacity: 1;"><rect rx="0" ry="0" x="-80.5" y="-22" width="161" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-70.5,-12)"><foreignObject width="141" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSynFilterOrValidate</div></foreignObject></g></g></g></g></g></g></svg></div>
<p>  您有强大的验证类，用于IP地址、电子邮件、简单的规则表达式、文本验证、密码强度验证……</p>
<p>  注意，一些数据库相关的验证已经存在，比如<code>TSynValidateUniqueField</code>，它继承自<code>TSynValidateRest</code>。</p>
<p>  当然，<code>mORMotUIEdit</code>单元自动处理<code>TSQLRecord</code>过滤(使用<code>TSQLFilter</code>类)和验证(通过<code>TSQLValidate</code>类)。</p>
<p>  字段验证过程在<code>TSQLRecord.Validate</code>中运行，而不是在<code>mORMotUIEdit</code>中（为更好的多层体系结构）。</p>
<p>  要使用它，您可以在<code>TSQLModel</code>初始化函数中添加一些过滤器/验证器：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">CreateFileModel</span><span class="hljs-params">(Owner: TSQLRest)</span>:</span> TSQLModel;
<span class="hljs-keyword">begin</span>
  result := TSQLModel.Create(Owner,
    @FileTabs,length(FileTabs),sizeof(FileTabs[<span class="hljs-number">0</span>]),[],
    TypeInfo(TFileAction),TypeInfo(TFileEvent),<span class="hljs-string">'synfile'</span>);
  TSQLFile.AddFilterOrValidate(<span class="hljs-string">'Name'</span>,TSQLFilterLowerCase);
  TSQLUser.AddFilterOrValidate(<span class="hljs-string">'Email'</span>,TSQLValidateEmail);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  作为一种替代方法，您可以重载以下方法：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-title">TSQLRecordAuthInfo</span> = <span class="hljs-keyword">class</span>(TSQLRecord)
  <span class="hljs-keyword">protected</span>
    <span class="hljs-keyword">class</span> <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">InternalDefineModel</span><span class="hljs-params">(Props: TSQLRecordProperties)</span>;</span> <span class="hljs-keyword">override</span>;
  ...

<span class="hljs-keyword">class</span> <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TSQLRecordAuthInfo</span>.<span class="hljs-title">InternalDefineModel</span><span class="hljs-params">(
  Props: TSQLRecordProperties)</span>;</span>
<span class="hljs-keyword">begin</span>
  AddFilterNotVoidText([<span class="hljs-string">'Logon'</span>,<span class="hljs-string">'HashedPassword'</span>]);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  在<code>TSQLRecord</code>定义中定义这种行为更有意义，这样所有模型都可以共享它。</p>
<p>  如果您想在ORM级执行一些文本字段长度验证或过滤，您可以使用<code>TSQLRecordProperties</code>的<code>SetMaxLengthValidatorForTextFields()</code>或<code>SetMaxLengthFilterForTextFields()</code>方法，或在模型级：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">CreateModel</span>:</span> TSQLModel;
<span class="hljs-keyword">begin</span>
  result := TSQLModel.Create([TSQLMyRecord1,TSQLMyRecord2]);
  result.SetMaxLengthValidatorForAllTextFields(true); <span class="hljs-comment">// "index n" is in UTF-8 bytes</span>
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  为了对某些内容过滤(转换)，您必须调用<code>aRecord.Filter()</code>方法和<code>aRecord.Validate()</code>来检测内容。</p>
<p>  例如，下面是<code>mORMotUIEdit.pas</code>单元过滤并验证用户输入的代码：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TRecordEditForm</span>.<span class="hljs-title">BtnSaveClick</span><span class="hljs-params">(Sender: TObject)</span>;</span>
 (...)
  <span class="hljs-comment">// perform all registered filtering</span>
  Rec.Filter(ModifiedFields);
  <span class="hljs-comment">// perform content validation</span>
  FieldIndex := -<span class="hljs-number">1</span>;
  ErrMsg := Rec.Validate(Client,ModifiedFields,@FieldIndex);
  <span class="hljs-keyword">if</span> ErrMsg&lt;&gt;<span class="hljs-string">''</span> <span class="hljs-keyword">then</span> <span class="hljs-keyword">begin</span>
    <span class="hljs-comment">// invalid field content -&gt; show message, focus component and abort saving</span>
    <span class="hljs-keyword">if</span> cardinal(FieldIndex)&lt;cardinal(length(fFieldComponents)) <span class="hljs-keyword">then</span> <span class="hljs-keyword">begin</span>
      C := fFieldComponents[FieldIndex];
      C.SetFocus;
      Application.ProcessMessages;
      ShowMessage(ErrMsg,format(sInvalidFieldN,[fFieldCaption[FieldIndex]]),true);
    <span class="hljs-keyword">end</span> <span class="hljs-keyword">else</span>
      ShowMessage(ErrMsg,format(sInvalidFieldN,[<span class="hljs-string">'?'</span>]),true);
  <span class="hljs-keyword">end</span> <span class="hljs-keyword">else</span>
    <span class="hljs-comment">// close window on success</span>
    ModalResult := mrOk;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  您可以在您的代码中使用过滤和验证，默认情况下，mORMot的CRUD操作不会调用过滤和验证方法。</p>
<h2 id="toc_44">5.7. ORM缓存<a class="vnote-anchor" href="#toc_44" data-anchor-icon="#"></a></h2>
<p>  以下是维基百科对“缓存”的定义：</p>
<p>  <em>在计算机工程中，缓存是一种透明地存储数据的组件，这样将来对这些数据的请求就可以更快地得到服务。存储在缓存中的数据可能是以前计算过的值，也可能是存储在其他地方的原始值的重复值。如果请求的数据包含在缓存中(缓存命中)，那么这个请求可以通过简单地读取缓存来提供，这相对更快。否则(缓存丢失)，数据必须重新计算或从其原始存储位置获取，这相对较慢。因此，可以从缓存中提供的请求数量越大，整个系统的性能就越快。</em></p>
<p>  <em>为了节约成本和高效使用数据，缓存相对较小。尽管如此，缓存在许多计算领域已经证明了自己，因为典型计算机应用程序中的访问模式具有局部性。如果再次请求最近已经请求的数据，引用会显示时间局部性。如果请求的数据物理上存储在已经请求的数据附近，引用会显示空间局部性。</em></p>
<p>  在我们的ORM框架中，性能从一开始就是我们的目标之一，因此在四个级别上实现了缓存：</p>
<ul>
<li>用于实现SQL语句准备的语句缓存，以及动态绑定的参数（请参阅查询参数和下面），注意，这个缓存不仅适用于SQlite3数据库引擎，也适用于其它外部引擎，请参见下面；</li>
<li>数据库级的全局JSON结果缓存，任何插入/更新时都会被全局缓存，参见下面；</li>
<li>在CRUD/RESTful为服务端指定的表或记录进行了记录缓存优化，见下文；</li>
<li>在客户端指定的表或记录的CRUD/RESTful级别上对记录缓存进行了调优，参见下面。</li>
</ul>
<p>  由于这些缓存功能，我们的框架能够极大地减少C/S请求数量，从而节省带宽和网络访问，并能在富客户端并发访问体系结构中很好地扩展。从这个角度来看，客户端-服务端ORM确实更有意义，与仅支持数据持久化和自动SQL生成的普通ORM相比，它有巨大的好处。</p>
<h2 id="toc_45">5.8. 计算字段<a class="vnote-anchor" href="#toc_45" data-anchor-icon="#"></a></h2>
<p>  处理一些计算字段通常很有用。也就是说，一个字段的值需要通过另外一些字段值计算而来。例如，如果您的错误代码设置为枚举值（存储在整数字段中），您可能希望获取对应的文本（存储在文本字段中）。或者，您可能希望从一些详情记录中自动计算总金额。</p>
<p>  字段计算不应该在服务器端完成。实际上，框架希望从客户端传入的JSON能直接设置到数据库，正如<code>mORMotSQLite3</code>单元的这段代码所述：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TSQLRestServerDB</span>.<span class="hljs-title">EngineUpdate</span><span class="hljs-params">(Table: TSQLRecordClass; ID: TID;
  <span class="hljs-keyword">const</span> SentData: RawUTF8)</span>:</span> boolean;
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">if</span> (self=<span class="hljs-keyword">nil</span>) <span class="hljs-keyword">or</span> (Table=<span class="hljs-keyword">nil</span>) <span class="hljs-keyword">or</span> (ID&lt;=<span class="hljs-number">0</span>) <span class="hljs-keyword">then</span>
    result := false <span class="hljs-keyword">else</span> <span class="hljs-keyword">begin</span>
    <span class="hljs-comment">// this SQL statement use :(inlined params): for all values</span>
    result := ExecuteFmt(<span class="hljs-string">'UPDATE % SET % WHERE RowID=:(%):;'</span>,
      [Table.RecordProps.SQLTableName,GetJSONObjectAsSQL(SentData,true,true),ID]);
    <span class="hljs-keyword">if</span> Assigned(OnUpdateEvent) <span class="hljs-keyword">then</span>
       OnUpdateEvent(self,seUpdate,Table,ID);
  <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  通过<code>GetJSONObjectAsSQL</code>过程，可以非常快速地将接收到的JSON内容直接转换为SQL的UPDATE语句。它没有使用任何<code>TSQLRecord</code>中介，因此没有在服务端处理字段计算。</p>
<p>  记录级的计算字段应该在客户端使用一些setter函数来完成。</p>
<p>  在发送到服务端之前，至少有三种方法可以更新字段值：</p>
<ul>
<li>通过对<code>TSQLRecord</code>属性使用一些专用的<code>setter</code>方法;</li>
<li>重载<code>TSQLRecord</code>的<code>ComputeFieldsBeforeWrite</code>虚拟方法。</li>
<li>如果需要处理复杂的计算字段（例如，如果需要修改另一条记录的某些属性），则应该采用专用的RESTful服务，参见下面。</li>
</ul>
<h3 id="toc_46">5.8.1. TSQLRecord的Setter方法<a class="vnote-anchor" href="#toc_46" data-anchor-icon="#"></a></h3>
<p>  例如，这里我们定义了一个名为<code>INVOICE</code>的新表，只有两个字段,一个是包含发票详细信息的动态数组，一个是总金额字段。动态数组属性将作为BLOB存储到数据库中，不需要额外的主/从表。</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  TInvoiceRec = <span class="hljs-keyword">record</span>
    Ident: RawUTF8;
    Amount: currency;
  <span class="hljs-keyword">end</span>;
  TInvoiceRecs = <span class="hljs-keyword">array</span> <span class="hljs-keyword">of</span> TInvoiceRec;
  <span class="hljs-title">TSQLInvoice</span> = <span class="hljs-keyword">class</span>(TSQLRecord)
  <span class="hljs-keyword">protected</span>
    fDetails: TInvoiceRecs;
    fTotal: Currency;
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">SetDetails</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Value: TInvoiceRecs)</span>;</span>
  <span class="hljs-keyword">published</span>
    <span class="hljs-keyword">property</span> Details: TInvoiceRecs <span class="hljs-keyword">read</span> fDetails <span class="hljs-keyword">write</span> SetDetails;
    <span class="hljs-keyword">property</span> Total: Currency <span class="hljs-keyword">read</span> fTotal;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  注意，<code>Total</code>属性没有任何<code>setter</code>（又称<code>write</code>语句），所以从ORM的角度来看，它是只读的。实际上，以下受保护的方法将从<code>Details</code>属性值中计算<code>Total</code>属性内容，然后对其进行修改：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TSQLInvoice</span>.<span class="hljs-title">SetDetails</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Value: TInvoiceRecs)</span>;</span>
<span class="hljs-keyword">var</span> i: integer;
<span class="hljs-keyword">begin</span>
  fDetails := Value;
  fTotal := <span class="hljs-number">0</span>;
  <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span> <span class="hljs-keyword">to</span> high(Value) <span class="hljs-keyword">do</span>
    fTotal := fTotal+Value[i].Amount;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  当将对象内容发送到服务器时，发送的JSON内容的<code>Total</code>值将包含预期值。</p>
<p>  注意，在这个实现中，必须显式地调用<code>SetDetails</code>。也就是说，您不仅应该直接修改<code>Details[]</code>数组内容，还应该通过临时数组，将其值赋给<code>Invoice.Details</code>，或者用如下代码强制更新：</p>
<pre><code class="lang-pascal hljs"> Invoice.Details := Invoice.Details; <span class="hljs-comment">// force Total calculation</span>
</code></pre>
<h3 id="toc_47">5.8.2. TSQLRecord.ComputeFieldsBeforeWrite<a class="vnote-anchor" href="#toc_47" data-anchor-icon="#"></a></h3>
<p>  即使<code>TSQLRecord</code>实例通常也不应该访问<code>TSQLRest</code>级，根据OOP原则，定义了以下虚拟方法：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-title">TSQLRecord</span> = <span class="hljs-keyword">class</span>(TObject)
  <span class="hljs-keyword">public</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">ComputeFieldsBeforeWrite</span><span class="hljs-params">(aRest: TSQLRest; aOccasion: TSQLEvent)</span>;</span> <span class="hljs-keyword">virtual</span>;
  (...)
</code></pre>
<p>  它将在客户端自动调用，在<code>TSQLRecord</code>内容发送到服务端之前，在添加或更新操作之前。</p>
<p>  事实上，在调用 <code>TSQLRecord.GetJSONValues</code>将JSON内容发送服务端之前，<code>TSQLRestClientURI.Add</code>/<code>Update</code>/<code>BatchAdd</code>/<code>BatchUpdate</code>方法将先调用此方法。</p>
<p>  在服务器端，如果某些业务逻辑涉及ORM，则使用<code>TSQLRestServer.Add</code>/<code>Update</code>方法也会调用<code>ComputeFieldsBeforeWrite</code>。</p>
<p>  默认情况下，此方法将使用服务器时间戳计算<code>TModTime</code>/<code>sftModTime</code>和<code>TCreateTime</code>/<code>sftCreateTime</code>属性值，如下所示：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TSQLRecord</span>.<span class="hljs-title">ComputeFieldsBeforeWrite</span><span class="hljs-params">(aRest: TSQLRest; aOccasion: TSQLEvent)</span>;</span>
<span class="hljs-keyword">var</span> F: integer;
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">if</span> (self&lt;&gt;<span class="hljs-keyword">nil</span>) <span class="hljs-keyword">and</span> (aRest&lt;&gt;<span class="hljs-keyword">nil</span>) <span class="hljs-keyword">then</span>
    <span class="hljs-keyword">with</span> RecordProps <span class="hljs-keyword">do</span> <span class="hljs-keyword">begin</span>
      <span class="hljs-keyword">if</span> HasModTimeFields <span class="hljs-keyword">then</span>
        <span class="hljs-keyword">for</span> F := <span class="hljs-number">0</span> <span class="hljs-keyword">to</span> high(FieldType) <span class="hljs-keyword">do</span>
        <span class="hljs-keyword">if</span> FieldType[f]=sftModTime <span class="hljs-keyword">then</span>
          SetInt64Prop(Self,Fields[F],aRest.ServerTimestamp);
      <span class="hljs-keyword">if</span> HasCreateTimeField <span class="hljs-keyword">and</span> (aOccasion=seAdd) <span class="hljs-keyword">then</span>
        <span class="hljs-keyword">for</span> F := <span class="hljs-number">0</span> <span class="hljs-keyword">to</span> high(FieldType) <span class="hljs-keyword">do</span>
        <span class="hljs-keyword">if</span> FieldType[f]=sftCreateTime <span class="hljs-keyword">then</span>
          SetInt64Prop(Self,Fields[F],aRest.ServerTimestamp);
    <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  您可以根据自己的需要重写此方法，从而调用这个继承实现来正确处理<code>TModTime</code>和<code>TCreateTime</code>发布属性。</p>
<h2 id="toc_48">5.9. 审计跟踪<a class="vnote-anchor" href="#toc_48" data-anchor-icon="#"></a></h2>
<p>  由于大多数CRUD操作都集中在我们的mORMot服务范围内，所以我们在ORM中实现了跟踪任何<code>TSQLRecord</code>变更（又名审计跟踪）的集成方法。</p>
<p>  跟踪业务对象的历史是软件建模的一个非常常见的需求，也是任何精确数据建模（比如域驱动设计）的必备条件。默认情况下，正如OOP模型所期望的那样，对对象的任何更改都会忘记该对象以前的任何状态。但是由于mORMot独有的变更跟踪特性，您可以持久化对象的历史。</p>
<h3 id="toc_49">5.9.1. 启用审计跟踪<a class="vnote-anchor" href="#toc_49" data-anchor-icon="#"></a></h3>
<p>  默认情况下，禁用跟踪更改功能，从而节省性能和磁盘使用。<br>
  但是，您可以在服务器端通过调用以下方法为任何类启用跟踪更改：</p>
<pre><code class="lang-pascal hljs"> aServer.TrackChanges([TSQLInvoice]);
</code></pre>
<p>  这一行将让<code>aServer: TSQLRestServer</code>监视所有CRUD操作，并将<code>TSQLInvoice</code>表的所有变更存储在<code>TSQLRecordHistory</code>表中。</p>
<p>  由于所有内容变更默认情况下都存储在这个表中（注意<code>TrackChanges()</code>方法接受一个数组类作为参数，并可多次调用），因此为历史存储定义几个表会更方便。稍后，可以定义一个外部数据库引擎来存储历史记录，例如在便宜的硬件（具有大容量硬盘）上，而您的主数据库可能由高端硬件（使用小容量的ssd）提供支持，参见下面。</p>
<p>  为此，您需要为历史存储定义自定义类，然后将其作为输入参数：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-title">TSQLRecordSecondaryHistory</span> = <span class="hljs-keyword">class</span>(TSQLRecordHistory);
 (...)
 aServer.TrackChanges([TSQLInvoice],TSQLRecordSecondaryHistory);
</code></pre>
<p>  然后，所有历史都将存储在这个<code>TSQLRecordSecondaryHistory</code>类中（名为<code>SecondaryHistory</code>的表中），而不是默认的<code>TSQLRecordHistory</code>类（<code>History</code>表）。</p>
<h3 id="toc_50">5.9.2. 对象的时光机<a class="vnote-anchor" href="#toc_50" data-anchor-icon="#"></a></h3>
<p>  一旦跟踪了对象的变更，您稍后可以浏览对象的历史，通过使用<code>TSQLRecordHistory.CreateHistory()</code>及<code>HistoryGetLast</code>、<code>HistoryCount</code>和<code>HistoryGet()</code>方法：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> aHist: TSQLRecordHistory;
    aInvoice: TSQLInvoice;
    aEvent: TSQLHistoryEvent; <span class="hljs-comment">// will be either heAdd, heUpdate or heDelete</span>
    aTimestamp: TModTime;
(...)
aInvoice := TSQLInvoice.Create;
aHist := TSQLRecordHistory.CreateHistory(aClient,TSQLInvoice,<span class="hljs-number">400</span>);
<span class="hljs-keyword">try</span>
  writeln(<span class="hljs-string">'Number of items in the record history: '</span>,aHist.HistoryCount);
  <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span> <span class="hljs-keyword">to</span> aHist.HistoryCount-<span class="hljs-number">1</span> <span class="hljs-keyword">do</span> <span class="hljs-keyword">begin</span>
    aHist.HistoryGet(i,aEvent,aTimestamp,aInvoice);
    writeln;
    writeln(<span class="hljs-string">'Event: '</span>,GetEnumName(TypeInfo(TSQLHistoryEvent),ord(aEvent))^);
    writeln(<span class="hljs-string">'Timestamp: '</span>,TTimeLogBits(aTimestamp).ToText);
    writeln(<span class="hljs-string">'Identifier: '</span>,aInvoice.Number);
    writeln(<span class="hljs-string">'Value: '</span>,aInvoice.GetJSONValues(true,true,soSelect));
  <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">finally</span>
  aHist.Free;
  aInvoice.Free;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  注意，有几个重载的<code>TSQLRecordHistory.HistoryGet()</code>版本用来检索记录值。</p>
<p>  因此，我们的ORM也变成了一个真正的时光机，为需要它的对象。</p>
<p>  这个特性通过<code>TSQLRecordHistory</code>表同时支持客户端和服务器端。</p>
<h3 id="toc_51">5.9.3. 历史打包自动化<a class="vnote-anchor" href="#toc_51" data-anchor-icon="#"></a></h3>
<p>  这个<code>TSQLRecordHistory</code>类实际上会在主数据库中创建一个历史表，定义如下：</p>
<table>
<thead>
<tr>
<th>ID : TID</th>
</tr>
</thead>
<tbody>
<tr>
<td>Event : TSQLHistoryEvent</td>
</tr>
<tr>
<td>History : TSQLRawBlob</td>
</tr>
<tr>
<td>ModifiedRecord : PtrInt</td>
</tr>
<tr>
<td>SentDataJSON : RawUTF8</td>
</tr>
<tr>
<td>Timestamp : TModTime</td>
</tr>
</tbody>
</table>
<p>  简而言之，通过ORM进行的任何修改都将存储在<code>TSQLRecordHistory</code>表中，变更字段的JSON对象存储在 <code>TSQLRecordHistory.SentDataJSON</code>字段中。</p>
<p>  按照设计，不处理直接使用SQL进行的变更，如果直接使用SQL语句，比如在应用程序或任何外部程序中直接使用<code>DELETE FROM…</code>或<code>UPDATE ... SET ...</code>，历史表都不会被更新。<br>
  实际上，ORM并没有设置任何数据库触发器来跟踪底层更改，这样会减慢进程，并导致我们想要遵循的持久性不可知论范式失效，例如支持MongoDB这样的NoSQL数据库。</p>
<p>  随着历史的增长，JSON内容可能会变得非常庞大，并使用大量重复内容填充磁盘空间。为了节省磁盘空间，在<code>TSQLRecordHistory.History</code>中，当记录达到JSON数据行定义的数量时，所有这些JSON内容将被集中压缩到一个BLOB中。</p>
<p>  您可以通过在代码中调用<code>TSQLRestServer.TrackChangesFlush()</code>来强制执行这个打包过程。调用该方法也会收到一些良好的边际效应：它将从数据库读出记录内容，然后添加一个<code>heUpdate</code>伪历史事件，保证字段值的变更跟踪计算的严谨性，确保变更审计的正确性。因此，历史库总能与数据库中存储的实际数据保持同步，即使外部SQL绕过ORM的CRUD方法，调用了不安全的<code>DELETE FROM…</code>或<code>UPDATE ... SET ...</code>语句。</p>
<p>  通过对注册方法使用一些可选参数，您可以调整如何为给定的<code>TSQLRecord</code>表定义打包操作：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TrackChanges</span><span class="hljs-params">(<span class="hljs-keyword">const</span> aTable: <span class="hljs-keyword">array</span> <span class="hljs-keyword">of</span> TSQLRecordClass;
  aTableHistory: TSQLRecordHistoryClass=<span class="hljs-keyword">nil</span>; aMaxHistoryRowBeforeBlob: integer=1000;
  aMaxHistoryRowPerRecord: integer=10; aMaxUncompressedBlobSize: integer=64*1024)</span>;</span> <span class="hljs-keyword">virtual</span>;
</code></pre>
<p>  查看此方法的文档（或其声明代码中的注释）以获得更多信息。</p>
<p>  默认为 <code>TSQLRecordHistory.SentDataJSON</code>记录每达到1000条就会调用<code>TSQLRestServer.TrackChangesFlush()</code>，然后将对这些记录每10条JSON行压缩成一个BLOB，确保每个BLOB记录未压缩前大小不会使用超过64 KB内存（实际使用数据库可能更小，因为压缩率非常高)。</p>
<h2 id="toc_52">5.10. 主/从复制<a class="vnote-anchor" href="#toc_52" data-anchor-icon="#"></a></h2>
<p>  正如在<code>TSQLRecord</code>字段定义中所述，ORM能够为任何<code>TSQLRecord</code>表维护一个修订号，以便与另一个远程<code>TSQLRestServer</code>实例轻松实现表同步。</p>
<p>  如果您定义了<code>TRecordVersion</code>发布属性，ORM核心将在所有写入操作之前递增修订号并填充该字段，还负责处理所有删除操作，以便在其它数据库中重复这些修改操作。</p>
<p>  此同步将实现精确的主/从复制，向待复制的表进行单向复制刷新，这样对于主数据库中特定表的每次写操作，都可以很容易地反映在一个或多个从数据库上，几乎没有速度影响和存储大小限制。</p>
<p>  除了使用这种同步方式，还可使用<code>WebSockets</code>通信的实时通知机制（见下文）。</p>
<h3 id="toc_53">5.10.1. 启用同步<a class="vnote-anchor" href="#toc_53" data-anchor-icon="#"></a></h3>
<p>  为了启用复制机制，您需要在<code>TSQLRecord</code>类中定义一个<code>TRecordVersion</code>类型的发布属性：</p>
<pre><code class="lang-pascal hljs">  <span class="hljs-title">TSQLRecordPeopleVersioned</span> = <span class="hljs-keyword">class</span>(TSQLRecordPeople)
  <span class="hljs-keyword">protected</span>
    fFirstName: RawUTF8;
    fLastName: RawUTF8;
    fVersion: TRecordVersion;
  <span class="hljs-keyword">published</span>
    <span class="hljs-keyword">property</span> FirstName: RawUTF8 <span class="hljs-keyword">read</span> fFirstName <span class="hljs-keyword">write</span> fFirstName;
    <span class="hljs-keyword">property</span> LastName: RawUTF8 <span class="hljs-keyword">read</span> fLastName <span class="hljs-keyword">write</span> fLastName;
    <span class="hljs-keyword">property</span> Version: TRecordVersion <span class="hljs-keyword">read</span> fVersion <span class="hljs-keyword">write</span> fVersion;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  每个<code>TSQLRecord</code>类只允许定义一个<code>TRecordVersion</code>字段，管理这个类型的多个字段没有实际意义。</p>
<p>  注意，对于大多数ORM进程来说，这个字段是“隐藏”的，通常<code>TSQLRest.Retrieve</code>不会获取<code>Version</code>属性，因为它是内部细节实现。如果要查找这个值，则必须在检索时显式地声明其字段名。任何<code>TRecordVersion</code>都被认为是“非普通字段”，就像BLOB字段一样，因此需要显式地来获取它的值。</p>
<p>  在实践中，所有<code>TSQLRest.Add</code>和<code>TSQLRest.Update</code>操作都将导致<code>TSQLRecordPeopleVersioned</code>字段值的增加， <code>TSQLRest.Delete</code>会将删除记录的<code>ID</code>登记到一个外部<code>TSQLRecordTableDelete</code>表，该表关联了<code>TRecordVersion</code>字段。</p>
<p>  由此：</p>
<ul>
<li><code>TRecordVersion</code>编号在<code>TSQLRestServer</code>级与所有包含此<code>TRecordVersion</code>发布字段的所有表共享;</li>
<li><code>TSQLRecordTableDelete</code>表与<code>TSQLRecordPeopleVersioned</code>均是<code>TSQLModel</code>的组成部分;</li>
<li>如果<code>TSQLRecordTableDelete</code>表不是<code>TSQLModel</code>的一部分，那么<code>TSQLRestServer</code>将添加它，但您最好显式地将它编写在数据模型中;</li>
<li>单个<code>TSQLRecordTableDelete</code>表将维护所有删除数据行的列表，以及所有包含<code>TRecordVersion</code>发布字段的表的列表;</li>
<li><code>TSQLRecordPeopleVersioned</code>表在<code>TSQLModel</code>中的代码顺序将会很重要，因为<code>TSQLRecordTableDelete.ID</code>将在数据库模型中使用这个表的位置序号来标记被删除行对应的表，标记的方法类似于<code>TRecordReference</code>和<code>TRecordReferenceToBeDeleted</code>。</li>
</ul>
<p>  在任何写操作期间，ORM内核都将处理所有的同步准备工作，除了<code>TRecordVersion</code>字段定义和全局<code>TSQLRecordTableDelete</code>表之外，没有什么特别需要维护或设置的。</p>
<h3 id="toc_54">5.10.2. 主从复制的实现<a class="vnote-anchor" href="#toc_54" data-anchor-icon="#"></a></h3>
<p>  要从一个<code>TSQLRestServer</code>实例同步复制<code>TSQLRecordPeopleVersioned</code>表，需调用以下方法：</p>
<pre><code class="lang-pascal hljs">aServer.RecordVersionSynchronizeSlave(TSQLRecordPeopleVersioned,aClient);
</code></pre>
<p>  这一行代码将通过<code>Client: TSQLRestClientURI</code>连接（可能是HTTP）请求远程服务器，获取最后一次调用以来的所有修改内容，然后更新本地<code>aServer:TSQLRestServer</code>数据库，使得本地的<code>TSQLRecordPeopleVersioned</code>表内容与远程<code>TSQLRestServer</code>主表一致。</p>
<p>  您可以从几个客户端安全地调用<code>TSQLRestServer.RecordVersionSynchronizeSlave</code>，将主数据库复制到几个数据库中。</p>
<p>  使用<code>TTimer</code>可以增强客户端应用的体验，以根据指定条件（例如，屏幕每500毫秒刷新一次）刷新显示数据。</p>
<p>  只有修改后的数据才通过连接进行传输，会进行两次REST/JSON处理（一次处理插入/更新，另一个处理删除），所有本地写的操作都将使用优化过的批处理来完成，参见下面。这意味着同步进程将尽可能地使c/s两端消耗的带宽和资源都最小化。</p>
<p>  实践中，您可以这样定义主数据库端：</p>
<pre><code class="lang-pascal hljs">  MasterServer := TSQLRestServerDB.Create(MasterModel,<span class="hljs-string">'master.db3'</span>);
  HttpMasterServer := TSQLHttpServer.Create(<span class="hljs-string">'8888'</span>,[MasterServer],<span class="hljs-string">'+'</span>,useBidirSocket);
</code></pre>
<p>  在从数据库端，HTTP客户端只需按通常的方式访问主数据库：</p>
<pre><code class="lang-pascal hljs"> MasterClient := TSQLHttpClientWebsockets.Create(<span class="hljs-string">'127.0.0.1'</span>,HTTP_DEFAULTPORT,MasterModel);
</code></pre>
<p>  当然，<code>MasterServer</code>和<code>MasterClient</code>实例中的模型应该保持一致，这就是为什么我们要使用同一个<code>MasterModel</code>变量名（可以定义在共享单元中）。</p>
<p>  假设从数据库端的定义如下：</p>
<pre><code class="lang-pascal hljs">SlaveServer := TSQLRestServerDB.Create(SlaveModel,<span class="hljs-string">'slave.db3'</span>);
</code></pre>
<p>  然后，对于给定的表，您可以使用一行代码通过从服务端发起同步复制：</p>
<pre><code class="lang-pascal hljs">SlaveServer.RecordVersionSynchronizeSlave(TSQLRecordPeopleVersioned,MasterClient);
</code></pre>
<p>  该命令处理同步复制的过程如下：</p>
<div class="mermaid-diagram"><svg id="mermaid-diagram-4" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 558 630" style="max-width:558px;"><style type="text/css" title="mermaid-svg-internal-css">/*  */
#mermaid-diagram-4 .node&gt;rect { ; }
#mermaid-diagram-4 .node text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-4 .edgeLabel text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-4 .cluster rect  { rx:4px; fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-4 .cyan&gt;rect, .cyan&gt;polygon, .cyan&gt;circle, .cyan&gt;ellipse { fill:#9ff;  stroke:#fff; }
/*  */
</style><g><g class="output"><g class="clusters"><g class="cluster" id="subGraph1" transform="translate(129.25,162.5)" style="opacity: 1;"><rect width="218.5" height="285" x="-109.25" y="-142.5"></rect><g class="label"><g transform="translate(0,0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"></div></foreignObject></g></g><text x="0" y="-128.5" fill="black" stroke="none" id="mermaid-diagram-4Text" style="text-anchor: middle;"> Master</text></g><g class="cluster" id="subGraph0" transform="translate(388.25,388.5)" style="opacity: 1;"><rect width="259.5" height="403" x="-129.75" y="-201.5"></rect><g class="label"><g transform="translate(0,0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"></div></foreignObject></g></g><text x="0" y="-187.5" fill="black" stroke="none" id="mermaid-diagram-4Text" style="text-anchor: middle;"> Slave</text></g></g><g class="edgePaths"><g class="edgePath" style="opacity: 1;"><path class="path" d="M143,113L143,150L143,187L335,232.49397590361446" marker-end="url(#arrowhead156)" style="fill:none"></path><defs><marker id="arrowhead156" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M335,228.484375L200,187L200,150L170.29577464788733,113" marker-end="url(#arrowhead157)" style="fill:none"></path><defs><marker id="arrowhead157" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M359.58474576271186,280L335.75,305L335.75,354L368.9578313253012,403" marker-end="url(#arrowhead158)" style="fill:none"></path><defs><marker id="arrowhead158" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M411.9698795180723,403L440.75,354L440.75,305L420.09322033898303,280" marker-end="url(#arrowhead159)" style="fill:none"></path><defs><marker id="arrowhead159" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M392,471L392,496L392,521" marker-end="url(#arrowhead160)" style="fill:none"></path><defs><marker id="arrowhead160" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M124.56338028169014,113L104.5,150L104.5,187L104.5,224" marker-end="url(#arrowhead161)" style="fill:none"></path><defs><marker id="arrowhead161" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" transform="translate(143,150)" style="opacity: 1;"><g transform="translate(-18.5,-12)" class="label"><foreignObject width="37" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">HTTP</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(200,150)" style="opacity: 1;"><g transform="translate(-18.5,-12)" class="label"><foreignObject width="37" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">HTTP</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(335.75,354)" style="opacity: 1;"><g transform="translate(-42.5,-24)" class="label"><foreignObject width="85" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">On Demand<br>Replication</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(440.75,354)" style="opacity: 1;"><g transform="translate(-42.5,-24)" class="label"><foreignObject width="85" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">On Demand<br>Replication</span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node cyan" id="A1" transform="translate(143,79)" style="opacity: 1;"><rect rx="0" ry="0" x="-57" y="-34" width="114" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-47,-24)"><foreignObject width="94" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">MasterServer<br>MasterModel</div></foreignObject></g></g></g><g class="node cyan" id="A2" transform="translate(104.5,246)" style="opacity: 1;"><rect rx="0" ry="0" x="-49.5" y="-22" width="99" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-39.5,-12)"><foreignObject width="79" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">master.db3</div></foreignObject></g></g></g><g class="node cyan" id="B1" transform="translate(392,246)" style="opacity: 1;"><rect rx="0" ry="0" x="-57" y="-34" width="114" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-47,-24)"><foreignObject width="94" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">MasterClient<br>MasterModel</div></foreignObject></g></g></g><g class="node cyan" id="B2" transform="translate(392,437)" style="opacity: 1;"><rect rx="0" ry="0" x="-51" y="-34" width="102" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-41,-24)"><foreignObject width="82" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">SlaveServer<br>SlaveModel</div></foreignObject></g></g></g><g class="node cyan" id="B3" transform="translate(392,543)" style="opacity: 1;"><rect rx="0" ry="0" x="-43" y="-22" width="86" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-33,-12)"><foreignObject width="66" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">slave.db3</div></foreignObject></g></g></g></g></g></g></svg></div>
<p>  当然，从服务端应该考虑同步时是只读的，否则版本号可能冲突，整个同步可能会失败。</p>
<p>  但是，如果需要，您可以实现对服务端的安全地级联复制，版本号将从主服务器传播到从服务器，并保证数据传播的一致性。</p>
<div class="mermaid-diagram"><svg id="mermaid-diagram-5" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 859.5 988" style="max-width:859.5px;"><style type="text/css" title="mermaid-svg-internal-css">/*  */
#mermaid-diagram-5 .node&gt;rect { ; }
#mermaid-diagram-5 .node text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-5 .edgeLabel text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-5 .cluster rect  { rx:4px; fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-5 .cyan&gt;rect, .cyan&gt;polygon, .cyan&gt;circle, .cyan&gt;ellipse { fill:#9ff;  stroke:#fff; }
/*  */
</style><g><g class="output"><g class="clusters"><g class="cluster" id="subGraph2" transform="translate(129.25,162.5)" style="opacity: 1;"><rect width="218.5" height="285" x="-109.25" y="-142.5"></rect><g class="label"><g transform="translate(0,0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"></div></foreignObject></g></g><text x="0" y="-128.5" fill="black" stroke="none" id="mermaid-diagram-5Text" style="text-anchor: middle;"> Master</text></g><g class="cluster" id="subGraph1" transform="translate(391,425)" style="opacity: 1;"><rect width="265" height="476" x="-132.5" y="-238"></rect><g class="label"><g transform="translate(0,0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"></div></foreignObject></g></g><text x="0" y="-224" fill="black" stroke="none" id="mermaid-diagram-5Text" style="text-anchor: middle;"> Slave 1</text></g><g class="cluster" id="subGraph0" transform="translate(672.5,746.5)" style="opacity: 1;"><rect width="258" height="403" x="-129" y="-201.5"></rect><g class="label"><g transform="translate(0,0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"></div></foreignObject></g></g><text x="0" y="-187.5" fill="black" stroke="none" id="mermaid-diagram-5Text" style="text-anchor: middle;"> Slave 2</text></g></g><g class="edgePaths"><g class="edgePath" style="opacity: 1;"><path class="path" d="M142.21830985915494,113L143,150L143,187L345.5,233.04046242774567" marker-end="url(#arrowhead280)" style="fill:none"></path><defs><marker id="arrowhead280" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M345.5,229.3925925925926L200,187L200,150L169.51408450704224,113" marker-end="url(#arrowhead281)" style="fill:none"></path><defs><marker id="arrowhead281" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M402.5,471L402.5,508L402.5,545L627.5,592.3262032085562" marker-end="url(#arrowhead282)" style="fill:none"></path><defs><marker id="arrowhead282" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M627.5,587.4621212121212L485,545L485,508L442.00704225352115,471" marker-end="url(#arrowhead283)" style="fill:none"></path><defs><marker id="arrowhead283" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M646.6949152542372,638L620,663L620,712L657.1927710843373,761" marker-end="url(#arrowhead284)" style="fill:none"></path><defs><marker id="arrowhead284" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M700.2048192771084,761L725,712L725,663L707.2033898305085,638" marker-end="url(#arrowhead285)" style="fill:none"></path><defs><marker id="arrowhead285" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M683,829L683,854L683,879" marker-end="url(#arrowhead286)" style="fill:none"></path><defs><marker id="arrowhead286" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M366.1949152542373,280L339.5,305L339.5,354L376.6927710843373,403" marker-end="url(#arrowhead287)" style="fill:none"></path><defs><marker id="arrowhead287" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M420.421686746988,403L446.25,354L446.25,305L427.7118644067797,280" marker-end="url(#arrowhead288)" style="fill:none"></path><defs><marker id="arrowhead288" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M372.8098591549296,471L340.5,508L340.5,545L340.5,582" marker-end="url(#arrowhead289)" style="fill:none"></path><defs><marker id="arrowhead289" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M123.78169014084507,113L104.5,150L104.5,187L104.5,224" marker-end="url(#arrowhead290)" style="fill:none"></path><defs><marker id="arrowhead290" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" transform="translate(143,150)" style="opacity: 1;"><g transform="translate(-18.5,-12)" class="label"><foreignObject width="37" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">HTTP</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(200,150)" style="opacity: 1;"><g transform="translate(-18.5,-12)" class="label"><foreignObject width="37" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">HTTP</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(402.5,508)" style="opacity: 1;"><g transform="translate(-18.5,-12)" class="label"><foreignObject width="37" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">HTTP</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(485,508)" style="opacity: 1;"><g transform="translate(-18.5,-12)" class="label"><foreignObject width="37" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">HTTP</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(620,712)" style="opacity: 1;"><g transform="translate(-42.5,-24)" class="label"><foreignObject width="85" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">On Demand<br>Replication</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(725,712)" style="opacity: 1;"><g transform="translate(-42.5,-24)" class="label"><foreignObject width="85" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">On Demand<br>Replication</span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(339.5,354)" style="opacity: 1;"><g transform="translate(-42.5,-24)" class="label"><foreignObject width="85" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">On Demand<br>Replication</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(446.25,354)" style="opacity: 1;"><g transform="translate(-42.5,-24)" class="label"><foreignObject width="85" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">On Demand<br>Replication</span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node cyan" id="A1" transform="translate(141.5,79)" style="opacity: 1;"><rect rx="0" ry="0" x="-57" y="-34" width="114" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-47,-24)"><foreignObject width="94" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">MasterServer<br>MasterModel</div></foreignObject></g></g></g><g class="node cyan" id="A2" transform="translate(104.5,246)" style="opacity: 1;"><rect rx="0" ry="0" x="-49.5" y="-22" width="99" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-39.5,-12)"><foreignObject width="79" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">master.db3</div></foreignObject></g></g></g><g class="node cyan" id="B1" transform="translate(402.5,246)" style="opacity: 1;"><rect rx="0" ry="0" x="-57" y="-34" width="114" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-47,-24)"><foreignObject width="94" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">MasterClient<br>MasterModel</div></foreignObject></g></g></g><g class="node cyan" id="B2" transform="translate(402.5,437)" style="opacity: 1;"><rect rx="0" ry="0" x="-55.5" y="-34" width="111" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-45.5,-24)"><foreignObject width="91" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">SlaveServer1<br>SlaveModel1</div></foreignObject></g></g></g><g class="node cyan" id="B3" transform="translate(340.5,604)" style="opacity: 1;"><rect rx="0" ry="0" x="-47" y="-22" width="94" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-37,-12)"><foreignObject width="74" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">slave1.db3</div></foreignObject></g></g></g><g class="node cyan" id="C1" transform="translate(683,604)" style="opacity: 1;"><rect rx="0" ry="0" x="-55.5" y="-34" width="111" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-45.5,-24)"><foreignObject width="91" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">SlaveClient1<br>SlaveModel1</div></foreignObject></g></g></g><g class="node cyan" id="C2" transform="translate(683,795)" style="opacity: 1;"><rect rx="0" ry="0" x="-55.5" y="-34" width="111" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-45.5,-24)"><foreignObject width="91" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">SlaveServer2<br>SlaveModel2</div></foreignObject></g></g></g><g class="node cyan" id="C3" transform="translate(683,901)" style="opacity: 1;"><rect rx="0" ry="0" x="-47" y="-22" width="94" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-37,-12)"><foreignObject width="74" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">slave2.db3</div></foreignObject></g></g></g></g></g></g></svg></div>
<p>  这种级联主/从复制设计可以与CQRS模式（命令查询职责分离）一起使用。实际上，<em>Slave 2</em>数据库可能是一个本地只读数据库实例，仅用于报告目的，例如营销人员或管理人员，而<em>Slave 1</em>可能是活动的只读数据库，所有本地业务流程都将在该数据库上读取其数据。因此，与Slave 1数据库相比，Slave 2实例需要被复制的频率可能要低得多，甚至可以实时被复制，就像我们现在看到的那样。</p>
<h3 id="toc_55">5.10.3. 实时同步<a class="vnote-anchor" href="#toc_55" data-anchor-icon="#"></a></h3>
<p>  有时，按需同步是不够的。</p>
<p>  例如，你可能需要：</p>
<ul>
<li>尽快同步一份不断变化的简短的数据项列表；</li>
<li>在你复制的数据中加入一些ACID-like的行为（比如处理钱！）</li>
<li>不是从GUI应用程序请求复制，而是从服务进程请求复制，所以不能使用TTimer；</li>
<li>将REST请求（ORM或服务进程）和主/从ORM复制合并到同一系统中，例如多线程应用中；</li>
<li>使用面向事件的持久性，并且期望从状态的所有变化中得到通知，参见下面。</li>
</ul>
<p>  在这种情况下，框架能够使用<code>WebSockets</code>和异步回调，持续进行主从复制，而不需要显式地请求同步数据。您需要使用<code>TSQLRestServer.RecordVersionSynchronizeMasterStart</code>、 <code>TSQLRestServer.RecordVersionSynchronizeSlaveStart</code>和<code>TSQLRestServer.RecordVersionSynchronizeSlaveStop</code>方法建立双向连接。</p>
<p>  第一步是在主HTTP服务器上启用<code>WebSockets</code>，所以初始化<code>TSQLHttpServer</code>类作为一个<code>useBidirSocket</code>类型的服务器，见下面：</p>
<pre><code class="lang-pascal hljs">  MasterServer := TSQLRestServerDB.Create(MasterModel,<span class="hljs-string">'master.db3'</span>);
  HttpMasterServer := TSQLHttpServer.Create(<span class="hljs-string">'8888'</span>,[MasterServer],<span class="hljs-string">'+'</span>,useBidirSocket);
  HttpMasterServer.WebSocketsEnable(Server,<span class="hljs-string">'PrivateAESEncryptionKey'</span>);
</code></pre>
<p>  HTTP从客户端也应该升级到支持<code>WebSockets</code>：</p>
<pre><code class="lang-pascal hljs">  MasterClient := TSQLHttpClientWebsockets.Create(<span class="hljs-string">'127.0.0.1'</span>,HTTP_DEFAULTPORT,MasterModel);
  MasterClient.WebSocketsUpgrade(<span class="hljs-string">'PrivateAESEncryptionKey'</span>);
</code></pre>
<p>  当然，模型在<code>MasterServer</code>和<code>MasterClient</code>实例中保持一致，正如<code>WebSockets</code>协议定义一样，这里是<code>'PrivateAESEncryptionKey'</code>私钥。</p>
<p>  然后在主服务端启用实时复制服务：</p>
<pre><code class="lang-pascal hljs">  MasterServer.RecordVersionSynchronizeMasterStart;
</code></pre>
<p>  这将在服务器端发布一个基于<code>IServiceRecordVersion</code>接口的服务，如下所示。</p>
<p>  假设从数据库是这样定义的：</p>
<pre><code class="lang-pascal hljs">  SlaveServer := TSQLRestServerDB.Create(SlaveModel,<span class="hljs-string">'slave.db3'</span>);
</code></pre>
<p>  （在本例中，<code>SlaveModel</code>可能与<code>MasterModel</code>不同，但<code>TSQLRecordPeopleVersioned</code>同时存在于两个模型中）</p>
<p>  然后，对于特定的表，您可以使用一行代码实现从客户端的实时复制：</p>
<pre><code class="lang-pascal hljs">SlaveServer.RecordVersionSynchronizeSlaveStart(TSQLRecordPeopleVersioned,MasterClient);
</code></pre>
<p>  上面的命令将订阅远程<code>MasterSlave</code>复制服务(即<code>IServiceRecordVersion</code>接口)，以接收关于<code>TSQLRecordPeopleVersioned</code> ORM表的任何更改，通过<code>WebSockets</code>的<code>MasterClient</code>连接，并将所有更新持久化到本地的<code>SlaveServer</code>数据库中。</p>
<p>  要停止此ORM表的实时通知，您可以执行：</p>
<pre><code class="lang-pascal hljs">SlaveServer.RecordVersionSynchronizeSlaveStop(TSQLRecordPeopleVersioned);
</code></pre>
<p>  即使您没有调用<code>RecordVersionSynchronizeSlaveStop()</code>，当<code>SlaveServer</code>实例被释放时，复制也会停止，而<code>MasterServer</code>也会从它的内部通知列表中取消该连接的订阅。</p>
<p>  这种典型的复制可以这样表示：</p>
<div class="mermaid-diagram"><svg id="mermaid-diagram-6" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 492.75 630" style="max-width:492.75px;"><style type="text/css" title="mermaid-svg-internal-css">/*  */
#mermaid-diagram-6 .node&gt;rect { ; }
#mermaid-diagram-6 .node text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-6 .edgeLabel text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-6 .cluster rect  { rx:4px; fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-6 .cyan&gt;rect, .cyan&gt;polygon, .cyan&gt;circle, .cyan&gt;ellipse { fill:#9ff;  stroke:#fff; }
/*  */
</style><g><g class="output"><g class="clusters"><g class="cluster" id="subGraph1" transform="translate(134.375,174.5)" style="opacity: 1;"><rect width="228.75" height="309" x="-114.375" y="-154.5"></rect><g class="label"><g transform="translate(0,0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"></div></foreignObject></g></g><text x="0" y="-140.5" fill="black" stroke="none" id="mermaid-diagram-6Text" style="text-anchor: middle;"> Master</text></g><g class="cluster" id="subGraph0" transform="translate(360.75,400.5)" style="opacity: 1;"><rect width="184" height="379" x="-92" y="-189.5"></rect><g class="label"><g transform="translate(0,0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"></div></foreignObject></g></g><text x="0" y="-175.5" fill="black" stroke="none" id="mermaid-diagram-6Text" style="text-anchor: middle;"> Slave</text></g></g><g class="edgePaths"><g class="edgePath" style="opacity: 1;"><path class="path" d="M152.65361445783134,113L171.25,162L171.25,211L303.75,252.2532981530343" marker-end="url(#arrowhead353)" style="fill:none"></path><defs><marker id="arrowhead353" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M360.75,304L360.75,329L360.75,366L360.75,403" marker-end="url(#arrowhead354)" style="fill:none"></path><defs><marker id="arrowhead354" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M360.75,471L360.75,496L360.75,521" marker-end="url(#arrowhead355)" style="fill:none"></path><defs><marker id="arrowhead355" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M126.84638554216868,113L108.25,162L108.25,211L108.25,248" marker-end="url(#arrowhead356)" style="fill:none"></path><defs><marker id="arrowhead356" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" transform="translate(171.25,162)" style="opacity: 1;"><g transform="translate(-43,-24)" class="label"><foreignObject width="86" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">WebSockets<br>TCP/IP</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(360.75,366)" style="opacity: 1;"><g transform="translate(-39,-12)" class="label"><foreignObject width="78" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">Replication</span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node cyan" id="A1" transform="translate(139.75,79)" style="opacity: 1;"><rect rx="0" ry="0" x="-57" y="-34" width="114" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-47,-24)"><foreignObject width="94" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">MasterServer<br>MasterModel</div></foreignObject></g></g></g><g class="node cyan" id="A2" transform="translate(108.25,270)" style="opacity: 1;"><rect rx="0" ry="0" x="-49.5" y="-22" width="99" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-39.5,-12)"><foreignObject width="79" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">master.db3</div></foreignObject></g></g></g><g class="node cyan" id="B1" transform="translate(360.75,270)" style="opacity: 1;"><rect rx="0" ry="0" x="-57" y="-34" width="114" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-47,-24)"><foreignObject width="94" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">MasterClient<br>MasterModel</div></foreignObject></g></g></g><g class="node cyan" id="B2" transform="translate(360.75,437)" style="opacity: 1;"><rect rx="0" ry="0" x="-51" y="-34" width="102" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-41,-24)"><foreignObject width="82" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">SlaveServer<br>SlaveModel</div></foreignObject></g></g></g><g class="node cyan" id="B3" transform="translate(360.75,543)" style="opacity: 1;"><rect rx="0" ry="0" x="-43" y="-22" width="86" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-33,-12)"><foreignObject width="66" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">slave.db3</div></foreignObject></g></g></g></g></g></g></svg></div>
<p>  实时通知的信息细节已经优化，以尽可能少的消耗带宽和资源。例如，如果向一个连接时间很短的从客户端发送变更通知，主服务端能够将这些变更集中到一个<code>WebSockets</code>帧中，并在一个批处理事务中整体更新到从数据库。</p>
<h3 id="toc_56">5.10.4. 复制示例<a class="vnote-anchor" href="#toc_56" data-anchor-icon="#"></a></h3>
<p>  我们可以考虑一个非常常见的企业基础架构：</p>
<div class="mermaid-diagram"><svg id="mermaid-diagram-7" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 1227.5 488" style="max-width:1227.5px;"><style type="text/css" title="mermaid-svg-internal-css">/*  */
#mermaid-diagram-7 .node&gt;rect { ; }
#mermaid-diagram-7 .node text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-7 .edgeLabel text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-7 .cluster rect  { rx:4px; fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-7 .cyan&gt;rect, .cyan&gt;polygon, .cyan&gt;circle, .cyan&gt;ellipse { fill:#9ff;  stroke:#fff; }
/*  */
</style><g><g class="output"><g class="clusters"><g class="cluster" id="subGraph2" transform="translate(129.75,162.5)" style="opacity: 1;"><rect width="219.5" height="285" x="-109.75" y="-142.5"></rect><g class="label"><g transform="translate(0,0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"></div></foreignObject></g></g><text x="0" y="-128.5" fill="black" stroke="none" id="mermaid-diagram-7Text" style="text-anchor: middle;"> Main Office</text></g><g class="cluster" id="subGraph1" transform="translate(455.5,317.5)" style="opacity: 1;"><rect width="392" height="261" x="-196" y="-130.5"></rect><g class="label"><g transform="translate(0,0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"></div></foreignObject></g></g><text x="0" y="-116.5" fill="black" stroke="none" id="mermaid-diagram-7Text" style="text-anchor: middle;"> Office A</text></g><g class="cluster" id="subGraph0" transform="translate(929.5,317.5)" style="opacity: 1;"><rect width="516" height="261" x="-258" y="-130.5"></rect><g class="label"><g transform="translate(0,0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"></div></foreignObject></g></g><text x="0" y="-116.5" fill="black" stroke="none" id="mermaid-diagram-7Text" style="text-anchor: middle;"> Office B</text></g></g><g class="edgePaths"><g class="edgePath" style="opacity: 1;"><path class="path" d="M171.29577464788733,113L201,150L201,187L890.5,242.84145504461222" marker-end="url(#arrowhead440)" style="fill:none"></path><defs><marker id="arrowhead440" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M144,113L144,150L144,187L416,238.51845906902088" marker-end="url(#arrowhead441)" style="fill:none"></path><defs><marker id="arrowhead441" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M890.5,258.3709677419355L743.5,305L743.5,342L743.5,379" marker-end="url(#arrowhead442)" style="fill:none"></path><defs><marker id="arrowhead442" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M893.771186440678,280L867.5,305L867.5,342L867.5,379" marker-end="url(#arrowhead443)" style="fill:none"></path><defs><marker id="arrowhead443" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M965.228813559322,280L991.5,305L991.5,342L991.5,379" marker-end="url(#arrowhead444)" style="fill:none"></path><defs><marker id="arrowhead444" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M968.5,258.3709677419355L1115.5,305L1115.5,342L1115.5,379" marker-end="url(#arrowhead445)" style="fill:none"></path><defs><marker id="arrowhead445" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M416,264.7943548387097L331.5,305L331.5,342L331.5,379" marker-end="url(#arrowhead446)" style="fill:none"></path><defs><marker id="arrowhead446" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M455.5,280L455.5,305L455.5,342L455.5,379" marker-end="url(#arrowhead447)" style="fill:none"></path><defs><marker id="arrowhead447" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M495,264.7943548387097L579.5,305L579.5,342L579.5,379" marker-end="url(#arrowhead448)" style="fill:none"></path><defs><marker id="arrowhead448" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M125.56338028169014,113L105.5,150L105.5,187L105.5,224" marker-end="url(#arrowhead449)" style="fill:none"></path><defs><marker id="arrowhead449" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" transform="translate(201,150)" style="opacity: 1;"><g transform="translate(-18.5,-12)" class="label"><foreignObject width="37" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">HTTP</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(144,150)" style="opacity: 1;"><g transform="translate(-18.5,-12)" class="label"><foreignObject width="37" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">HTTP</span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(1115.5,342)" style="opacity: 1;"><g transform="translate(-47,-12)" class="label"><foreignObject width="94" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">local network</span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(579.5,342)" style="opacity: 1;"><g transform="translate(-47,-12)" class="label"><foreignObject width="94" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">local network</span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node cyan" id="M1" transform="translate(144,79)" style="opacity: 1;"><rect rx="0" ry="0" x="-32" y="-34" width="64" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-22,-24)"><foreignObject width="44" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Main<br>Server</div></foreignObject></g></g></g><g class="node cyan" id="M2" transform="translate(105.5,246)" style="opacity: 1;"><rect rx="0" ry="0" x="-50.5" y="-22" width="101" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40.5,-12)"><foreignObject width="81" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">External DB</div></foreignObject></g></g></g><g class="node cyan" id="A" transform="translate(455.5,246)" style="opacity: 1;"><rect rx="0" ry="0" x="-39.5" y="-34" width="79" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-29.5,-24)"><foreignObject width="59" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local<br>Server A</div></foreignObject></g></g></g><g class="node cyan" id="A1" transform="translate(331.5,401)" style="opacity: 1;"><rect rx="0" ry="0" x="-37" y="-22" width="74" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-27,-12)"><foreignObject width="54" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Client 1</div></foreignObject></g></g></g><g class="node cyan" id="A2" transform="translate(455.5,401)" style="opacity: 1;"><rect rx="0" ry="0" x="-37" y="-22" width="74" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-27,-12)"><foreignObject width="54" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Client 2</div></foreignObject></g></g></g><g class="node cyan" id="A3" transform="translate(579.5,401)" style="opacity: 1;"><rect rx="0" ry="0" x="-37" y="-22" width="74" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-27,-12)"><foreignObject width="54" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Client 3</div></foreignObject></g></g></g><g class="node cyan" id="B" transform="translate(929.5,246)" style="opacity: 1;"><rect rx="0" ry="0" x="-39" y="-34" width="78" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-29,-24)"><foreignObject width="58" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local<br>Server B</div></foreignObject></g></g></g><g class="node cyan" id="B1" transform="translate(743.5,401)" style="opacity: 1;"><rect rx="0" ry="0" x="-37" y="-22" width="74" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-27,-12)"><foreignObject width="54" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Client 1</div></foreignObject></g></g></g><g class="node cyan" id="B2" transform="translate(867.5,401)" style="opacity: 1;"><rect rx="0" ry="0" x="-37" y="-22" width="74" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-27,-12)"><foreignObject width="54" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Client 2</div></foreignObject></g></g></g><g class="node cyan" id="B3" transform="translate(991.5,401)" style="opacity: 1;"><rect rx="0" ry="0" x="-37" y="-22" width="74" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-27,-12)"><foreignObject width="54" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Client 3</div></foreignObject></g></g></g><g class="node cyan" id="B4" transform="translate(1115.5,401)" style="opacity: 1;"><rect rx="0" ry="0" x="-37" y="-22" width="74" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-27,-12)"><foreignObject width="54" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Client 4</div></foreignObject></g></g></g></g></g></g></svg></div>
<p>  这种带有中心主办公室和本地办公网的安装将从主/从复制中获益，使用简单的重定向（见下文），即使在Internet网络故障的情况下，它也会继续工作。REST重定向将期望100%的上行链接，这在某些情况下可能非常关键。</p>
<p>  因此，您可以通过以下几种方式实现复制：</p>
<ul>
<li>无论是主办公室还是主服务器，任何写入操作都将被推送到主服务器，而本地办公室只拥有信息的复制副本，缺点是，如果网络出现故障，本地办公室将被限制只能读取数据；</li>
</ul>
<div class="mermaid-diagram"><svg id="mermaid-diagram-8" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 750 488" style="max-width:750px;"><style type="text/css" title="mermaid-svg-internal-css">/*  */
#mermaid-diagram-8 .node&gt;rect { ; }
#mermaid-diagram-8 .node text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-8 .edgeLabel text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-8 .cluster rect  { rx:4px; fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-8 .cyan&gt;rect, .cyan&gt;polygon, .cyan&gt;circle, .cyan&gt;ellipse { fill:#9ff;  stroke:#fff; }
/*  */
</style><g><g class="output"><g class="clusters"><g class="cluster" id="subGraph2" transform="translate(350.25,330)" style="opacity: 1;"><rect width="582.5" height="236" x="-291.25" y="-118"></rect><g class="label"><g transform="translate(0,0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"></div></foreignObject></g></g><text x="0" y="-104" fill="black" stroke="none" id="mermaid-diagram-8Text" style="text-anchor: middle;"> Main Office</text></g><g class="cluster" id="subGraph1" transform="translate(542.5,79)" style="opacity: 1;"><rect width="335" height="118" x="-167.5" y="-59"></rect><g class="label"><g transform="translate(0,0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"></div></foreignObject></g></g><text x="0" y="-45" fill="black" stroke="none" id="mermaid-diagram-8Text" style="text-anchor: middle;"> Office B</text></g><g class="cluster" id="subGraph0" transform="translate(187.5,79)" style="opacity: 1;"><rect width="335" height="118" x="-167.5" y="-59"></rect><g class="label"><g transform="translate(0,0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"></div></foreignObject></g></g><text x="0" y="-45" fill="black" stroke="none" id="mermaid-diagram-8Text" style="text-anchor: middle;"> Office A</text></g></g><g class="edgePaths"><g class="edgePath" style="opacity: 1;"><path class="path" d="M109,113L109,138L109,175L109,212L333.25,263.6321951219512" marker-end="url(#arrowhead511)" style="fill:none"></path><defs><marker id="arrowhead511" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M266.5,113L266.5,138L266.5,175L266.5,212L333.25,251.88101265822786" marker-end="url(#arrowhead512)" style="fill:none"></path><defs><marker id="arrowhead512" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M464,113L464,138L464,175L464,212L397.25,251.88101265822786" marker-end="url(#arrowhead513)" style="fill:none"></path><defs><marker id="arrowhead513" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M621.5,113L621.5,138L621.5,175L621.5,212L397.25,263.6321951219512" marker-end="url(#arrowhead514)" style="fill:none"></path><defs><marker id="arrowhead514" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M333.25,279.69044879171463L148,330L148,355" marker-end="url(#arrowhead515)" style="fill:none"></path><defs><marker id="arrowhead515" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M397.25,281.05592543275634L553,330L553,355" marker-end="url(#arrowhead516)" style="fill:none"></path><defs><marker id="arrowhead516" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(266.5,175)" style="opacity: 1;"><g transform="translate(-39,-12)" class="label"><foreignObject width="78" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">Replication</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(464,175)" style="opacity: 1;"><g transform="translate(-39,-12)" class="label"><foreignObject width="78" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">Replication</span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node cyan" id="MS" transform="translate(365.25,271)" style="opacity: 1;"><rect rx="0" ry="0" x="-32" y="-34" width="64" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-22,-24)"><foreignObject width="44" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Main<br>Server</div></foreignObject></g></g></g><g class="node cyan" id="DA" transform="translate(148,389)" style="opacity: 1;"><rect rx="0" ry="0" x="-54" y="-34" width="108" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-44,-24)"><foreignObject width="88" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local Data A<br>Read/Write</div></foreignObject></g></g></g><g class="node cyan" id="DB" transform="translate(553,389)" style="opacity: 1;"><rect rx="0" ry="0" x="-53.5" y="-34" width="107" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.5,-24)"><foreignObject width="87" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local Data B<br>Read/Write</div></foreignObject></g></g></g><g class="node cyan" id="AA" transform="translate(464,79)" style="opacity: 1;"><rect rx="0" ry="0" x="-54" y="-34" width="108" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-44,-24)"><foreignObject width="88" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local Data A<br>Read Only</div></foreignObject></g></g></g><g class="node cyan" id="AB" transform="translate(621.5,79)" style="opacity: 1;"><rect rx="0" ry="0" x="-53.5" y="-34" width="107" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.5,-24)"><foreignObject width="87" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local Data B<br>Read Only</div></foreignObject></g></g></g><g class="node cyan" id="BA" transform="translate(109,79)" style="opacity: 1;"><rect rx="0" ry="0" x="-54" y="-34" width="108" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-44,-24)"><foreignObject width="88" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local Data A<br>Read Only</div></foreignObject></g></g></g><g class="node cyan" id="BB" transform="translate(266.5,79)" style="opacity: 1;"><rect rx="0" ry="0" x="-53.5" y="-34" width="107" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.5,-24)"><foreignObject width="87" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local Data B<br>Read Only</div></foreignObject></g></g></g></g></g></g></svg></div>
<ul>
<li>或者，每个本地办公室可以将自己的数据放在一个专用表中，同步到主数据库；主办公室将复制（作为从属）每个本地服务器的私有数据；此外，主服务器获取的所有数据可以进一步复制到其他本地办公室，并且仍然可以只读模式访问，在网络故障的情况下，所有数据在本地服务器上可用，本地私有表仍然可写。</li>
</ul>
<div class="mermaid-diagram"><svg id="mermaid-diagram-9" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 750 536" style="max-width:750px;"><style type="text/css" title="mermaid-svg-internal-css">/*  */
#mermaid-diagram-9 .node&gt;rect { ; }
#mermaid-diagram-9 .node text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-9 .edgeLabel text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-9 .cluster rect  { rx:4px; fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-9 .cyan&gt;rect, .cyan&gt;polygon, .cyan&gt;circle, .cyan&gt;ellipse { fill:#9ff;  stroke:#fff; }
/*  */
</style><g><g class="output"><g class="clusters"><g class="cluster" id="subGraph2" transform="translate(350.25,366)" style="opacity: 1;"><rect width="582.5" height="260" x="-291.25" y="-130"></rect><g class="label"><g transform="translate(0,0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"></div></foreignObject></g></g><text x="0" y="-116" fill="black" stroke="none" id="mermaid-diagram-9Text" style="text-anchor: middle;"> Main Office</text></g><g class="cluster" id="subGraph1" transform="translate(542.5,91)" style="opacity: 1;"><rect width="335" height="142" x="-167.5" y="-71"></rect><g class="label"><g transform="translate(0,0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"></div></foreignObject></g></g><text x="0" y="-57" fill="black" stroke="none" id="mermaid-diagram-9Text" style="text-anchor: middle;"> Office B</text></g><g class="cluster" id="subGraph0" transform="translate(187.5,91)" style="opacity: 1;"><rect width="335" height="142" x="-167.5" y="-71"></rect><g class="label"><g transform="translate(0,0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"></div></foreignObject></g></g><text x="0" y="-57" fill="black" stroke="none" id="mermaid-diagram-9Text" style="text-anchor: middle;"> Office A</text></g></g><g class="edgePaths"><g class="edgePath" style="opacity: 1;"><path class="path" d="M109,137L109,162L109,199L109,236L333.25,287.6321951219512" marker-end="url(#arrowhead578)" style="fill:none"></path><defs><marker id="arrowhead578" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M266.5,137L266.5,162L266.5,199L266.5,236L333.25,275.88101265822786" marker-end="url(#arrowhead579)" style="fill:none"></path><defs><marker id="arrowhead579" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M464,137L464,162L464,199L464,236L397.25,275.88101265822786" marker-end="url(#arrowhead580)" style="fill:none"></path><defs><marker id="arrowhead580" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M621.5,137L621.5,162L621.5,199L621.5,236L397.25,287.6321951219512" marker-end="url(#arrowhead581)" style="fill:none"></path><defs><marker id="arrowhead581" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M333.25,303.69044879171463L148,354L148,379" marker-end="url(#arrowhead582)" style="fill:none"></path><defs><marker id="arrowhead582" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M397.25,305.05592543275634L553,354L553,379" marker-end="url(#arrowhead583)" style="fill:none"></path><defs><marker id="arrowhead583" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(266.5,199)" style="opacity: 1;"><g transform="translate(-39,-12)" class="label"><foreignObject width="78" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">Replication</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(464,199)" style="opacity: 1;"><g transform="translate(-39,-12)" class="label"><foreignObject width="78" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">Replication</span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node cyan" id="MS" transform="translate(365.25,295)" style="opacity: 1;"><rect rx="0" ry="0" x="-32" y="-34" width="64" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-22,-24)"><foreignObject width="44" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Main<br>Server</div></foreignObject></g></g></g><g class="node cyan" id="DA" transform="translate(148,425)" style="opacity: 1;"><rect rx="0" ry="0" x="-54" y="-46" width="108" height="92"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-44,-36)"><foreignObject width="88" height="72"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local Data A<br>Reference<br>Read Only</div></foreignObject></g></g></g><g class="node cyan" id="DB" transform="translate(553,425)" style="opacity: 1;"><rect rx="0" ry="0" x="-53.5" y="-46" width="107" height="92"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.5,-36)"><foreignObject width="87" height="72"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local Data B<br>Reference<br>Read Only</div></foreignObject></g></g></g><g class="node cyan" id="AA" transform="translate(464,91)" style="opacity: 1;"><rect rx="0" ry="0" x="-54" y="-46" width="108" height="92"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-44,-36)"><foreignObject width="88" height="72"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local Data A<br>Business<br>Read Only</div></foreignObject></g></g></g><g class="node cyan" id="AB" transform="translate(621.5,91)" style="opacity: 1;"><rect rx="0" ry="0" x="-53.5" y="-46" width="107" height="92"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.5,-36)"><foreignObject width="87" height="72"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local Data B<br>Business<br>Read/Write</div></foreignObject></g></g></g><g class="node cyan" id="BA" transform="translate(109,91)" style="opacity: 1;"><rect rx="0" ry="0" x="-54" y="-46" width="108" height="92"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-44,-36)"><foreignObject width="88" height="72"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local Data A<br>Business<br>Read/Write</div></foreignObject></g></g></g><g class="node cyan" id="BB" transform="translate(266.5,91)" style="opacity: 1;"><rect rx="0" ry="0" x="-53.5" y="-46" width="107" height="92"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.5,-36)"><foreignObject width="87" height="72"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local Data B<br>Business<br>Read Only</div></foreignObject></g></g></g></g></g></g></svg></div>
<p>  当然，第二种解决方案似乎更可取，尽管实现起来有点困难。所有本地办公室都可以在自己的私有数据上离线工作，并可以以只读方式访问所有其他数据，这将是一个巨大的ROI（投资回报）。</p>
<p>  由于使用复制的好处，中心主服务器的压力将会更小，因为大部分过程将在本地服务器中进行，主办公室服务器将仅用于备份共享数据和采集其他远程数据库，只需要一个小的网络带宽（比纯web解决方案少得多），CPU/存储资源将是最小的。</p>
<p>  如果需要，实时同步将允许远程办公室数据库以“接近实时”地复制主办公室数据，而写操作仍可安全地在主办公室进行。另一个级联复制可以在任何节点内发生，根据需要刷新（例如1小时周期）来实现CQRS模式（命令查询责任隔离）。</p>
<div class="mermaid-diagram"><svg id="mermaid-diagram-10" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 395 750" style="max-width:395px;"><style type="text/css" title="mermaid-svg-internal-css">/*  */
#mermaid-diagram-10 .node&gt;rect { ; }
#mermaid-diagram-10 .node text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-10 .edgeLabel text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-10 .cluster rect  { rx:4px; fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-10 .cyan&gt;rect, .cyan&gt;polygon, .cyan&gt;circle, .cyan&gt;ellipse { fill:#9ff;  stroke:#fff; }
/*  */
</style><g><g class="output"><g class="clusters"><g class="cluster" id="subGraph1" transform="translate(187.5,580)" style="opacity: 1;"><rect width="335" height="260" x="-167.5" y="-130"></rect><g class="label"><g transform="translate(0,0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"></div></foreignObject></g></g><text x="0" y="-116" fill="black" stroke="none" id="mermaid-diagram-10Text" style="text-anchor: middle;"> Main Office</text></g><g class="cluster" id="subGraph0" transform="translate(187.5,186)" style="opacity: 1;"><rect width="335" height="332" x="-167.5" y="-166"></rect><g class="label"><g transform="translate(0,0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"></div></foreignObject></g></g><text x="0" y="-152" fill="black" stroke="none" id="mermaid-diagram-10Text" style="text-anchor: middle;"> Office B</text></g></g><g class="edgePaths"><g class="edgePath" style="opacity: 1;"><path class="path" d="M109,327L109,352L109,401L109,450L155.75,485.0253968253968" marker-end="url(#arrowhead643)" style="fill:none"></path><defs><marker id="arrowhead643" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M266.5,327L266.5,352L266.5,401L266.5,450L219.75,485.0253968253968" marker-end="url(#arrowhead644)" style="fill:none"></path><defs><marker id="arrowhead644" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M266.5,137L266.5,186L266.5,235" marker-end="url(#arrowhead645)" style="fill:none"></path><defs><marker id="arrowhead645" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M109,137L109,186L109,235" marker-end="url(#arrowhead646)" style="fill:none"></path><defs><marker id="arrowhead646" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M155.75,532.9746031746032L109,568L109,593" marker-end="url(#arrowhead647)" style="fill:none"></path><defs><marker id="arrowhead647" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M219.75,532.9746031746032L266.5,568L266.5,593" marker-end="url(#arrowhead648)" style="fill:none"></path><defs><marker id="arrowhead648" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" transform="translate(109,401)" style="opacity: 1;"><g transform="translate(-39,-24)" class="label"><foreignObject width="78" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">RealTime<br>Replication</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(266.5,401)" style="opacity: 1;"><g transform="translate(-39,-24)" class="label"><foreignObject width="78" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">RealTime<br>Replication</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(266.5,186)" style="opacity: 1;"><g transform="translate(-40.5,-24)" class="label"><foreignObject width="81" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">OnDemand<br>Replication</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(109,186)" style="opacity: 1;"><g transform="translate(-40.5,-24)" class="label"><foreignObject width="81" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">OnDemand<br>Replication</span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node cyan" id="MS" transform="translate(187.75,509)" style="opacity: 1;"><rect rx="0" ry="0" x="-32" y="-34" width="64" height="68"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-22,-24)"><foreignObject width="44" height="48"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Main<br>Server</div></foreignObject></g></g></g><g class="node cyan" id="DA" transform="translate(109,639)" style="opacity: 1;"><rect rx="0" ry="0" x="-54" y="-46" width="108" height="92"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-44,-36)"><foreignObject width="88" height="72"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local Data A<br>Reference<br>Read Only</div></foreignObject></g></g></g><g class="node cyan" id="DB" transform="translate(266.5,639)" style="opacity: 1;"><rect rx="0" ry="0" x="-53.5" y="-46" width="107" height="92"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.5,-36)"><foreignObject width="87" height="72"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local Data B<br>Reference<br>Read Only</div></foreignObject></g></g></g><g class="node cyan" id="AA" transform="translate(266.5,91)" style="opacity: 1;"><rect rx="0" ry="0" x="-53.5" y="-46" width="107" height="92"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.5,-36)"><foreignObject width="87" height="72"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local Data B<br>Reporting<br>Read Only</div></foreignObject></g></g></g><g class="node cyan" id="AB" transform="translate(266.5,281)" style="opacity: 1;"><rect rx="0" ry="0" x="-53.5" y="-46" width="107" height="92"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.5,-36)"><foreignObject width="87" height="72"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local Data B<br>Business<br>Read/Write</div></foreignObject></g></g></g><g class="node cyan" id="BA" transform="translate(109,91)" style="opacity: 1;"><rect rx="0" ry="0" x="-54" y="-46" width="108" height="92"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-44,-36)"><foreignObject width="88" height="72"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local Data A<br>Reporting<br>Read Only</div></foreignObject></g></g></g><g class="node cyan" id="BB" transform="translate(109,281)" style="opacity: 1;"><rect rx="0" ry="0" x="-54" y="-46" width="108" height="92"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-44,-36)"><foreignObject width="88" height="72"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Local Data A<br>Business<br>Read Only</div></foreignObject></g></g></g></g></g></g></svg></div>
<p>  按照CQRS模式，在那些只读的“Reporting”复制数据库中可能会发生一些要求很高的查询，而不会影响到主本地数据库，而所有实际的“Business”都将在主本地数据库中进行。</p>

    </div>
</div>
</div>

<div id="container-floating" style="display:none;" class="d-none d-md-block d-xl-block">
    <div id="floating-button" onclick="toggleMore()">
        <p id="floating-more" class="more">&gt;</p>
    </div>
</div>

<!--
<div class="footer" id="vnote-footer">
    <p>Generated by <em><a href="https://tamlok.github.io/vnote/">VNote</a></em>.</p>
</div>
-->
</body>
</html>
